tryassay 0.22.0 → 0.22.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 (130) hide show
  1. package/demo/css/style.css +495 -836
  2. package/demo/index.html +40 -184
  3. package/demo/js/chat.js +385 -142
  4. package/demo/js/preview.js +456 -0
  5. package/demo/js/sse-client.js +262 -135
  6. package/demo/js/state.js +11 -1
  7. package/demo/js/timeline.js +57 -371
  8. package/dist/api/server.d.ts +2 -0
  9. package/dist/api/server.js +63 -19
  10. package/dist/api/server.js.map +1 -1
  11. package/dist/cli.js +2 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/assess.d.ts +2 -0
  14. package/dist/commands/assess.js +132 -164
  15. package/dist/commands/assess.js.map +1 -1
  16. package/dist/commands/demo.js +259 -9
  17. package/dist/commands/demo.js.map +1 -1
  18. package/dist/lib/__tests__/arithmetic-quick-test.d.ts +6 -0
  19. package/dist/lib/__tests__/arithmetic-quick-test.js +197 -0
  20. package/dist/lib/__tests__/arithmetic-quick-test.js.map +1 -0
  21. package/dist/lib/__tests__/arithmetic-real-llm-test.d.ts +13 -0
  22. package/dist/lib/__tests__/arithmetic-real-llm-test.js +284 -0
  23. package/dist/lib/__tests__/arithmetic-real-llm-test.js.map +1 -0
  24. package/dist/lib/__tests__/arithmetic-value-demo.d.ts +10 -0
  25. package/dist/lib/__tests__/arithmetic-value-demo.js +193 -0
  26. package/dist/lib/__tests__/arithmetic-value-demo.js.map +1 -0
  27. package/dist/lib/__tests__/flow-to-claims.test.d.ts +1 -0
  28. package/dist/lib/__tests__/flow-to-claims.test.js +91 -0
  29. package/dist/lib/__tests__/flow-to-claims.test.js.map +1 -0
  30. package/dist/lib/__tests__/formal-verifier-api-misuse.test.d.ts +9 -0
  31. package/dist/lib/__tests__/formal-verifier-api-misuse.test.js +391 -0
  32. package/dist/lib/__tests__/formal-verifier-api-misuse.test.js.map +1 -0
  33. package/dist/lib/__tests__/formal-verifier-arithmetic.test.d.ts +7 -0
  34. package/dist/lib/__tests__/formal-verifier-arithmetic.test.js +318 -0
  35. package/dist/lib/__tests__/formal-verifier-arithmetic.test.js.map +1 -0
  36. package/dist/lib/__tests__/intent-extractor.test.d.ts +1 -0
  37. package/dist/lib/__tests__/intent-extractor.test.js +97 -0
  38. package/dist/lib/__tests__/intent-extractor.test.js.map +1 -0
  39. package/dist/lib/__tests__/intent-reviewer.test.d.ts +1 -0
  40. package/dist/lib/__tests__/intent-reviewer.test.js +55 -0
  41. package/dist/lib/__tests__/intent-reviewer.test.js.map +1 -0
  42. package/dist/lib/__tests__/mr-gsm8k-benchmark.d.ts +11 -0
  43. package/dist/lib/__tests__/mr-gsm8k-benchmark.js +224 -0
  44. package/dist/lib/__tests__/mr-gsm8k-benchmark.js.map +1 -0
  45. package/dist/lib/anthropic.js +25 -33
  46. package/dist/lib/anthropic.js.map +1 -1
  47. package/dist/lib/assessment-reporter.js +9 -13
  48. package/dist/lib/assessment-reporter.js.map +1 -1
  49. package/dist/lib/claim-extractor.js +10 -19
  50. package/dist/lib/claim-extractor.js.map +1 -1
  51. package/dist/lib/code-verifier.js +16 -36
  52. package/dist/lib/code-verifier.js.map +1 -1
  53. package/dist/lib/constraint-engine.js +10 -19
  54. package/dist/lib/constraint-engine.js.map +1 -1
  55. package/dist/lib/formal-verifier.d.ts +1 -1
  56. package/dist/lib/formal-verifier.js +454 -0
  57. package/dist/lib/formal-verifier.js.map +1 -1
  58. package/dist/lib/guided-generator.js +19 -37
  59. package/dist/lib/guided-generator.js.map +1 -1
  60. package/dist/lib/intent-extractor.d.ts +47 -0
  61. package/dist/lib/intent-extractor.js +427 -0
  62. package/dist/lib/intent-extractor.js.map +1 -0
  63. package/dist/lib/intent-reviewer.d.ts +14 -0
  64. package/dist/lib/intent-reviewer.js +148 -0
  65. package/dist/lib/intent-reviewer.js.map +1 -0
  66. package/dist/lib/intent-types.d.ts +89 -0
  67. package/dist/lib/intent-types.js +5 -0
  68. package/dist/lib/intent-types.js.map +1 -0
  69. package/dist/lib/inventory-extractor.js +9 -22
  70. package/dist/lib/inventory-extractor.js.map +1 -1
  71. package/dist/lib/llm-provider.d.ts +23 -0
  72. package/dist/lib/llm-provider.js +130 -0
  73. package/dist/lib/llm-provider.js.map +1 -0
  74. package/dist/lib/remediator.js +20 -28
  75. package/dist/lib/remediator.js.map +1 -1
  76. package/dist/lib/requirements-generator.js +14 -19
  77. package/dist/lib/requirements-generator.js.map +1 -1
  78. package/dist/lib/spec-synthesizer.js +10 -19
  79. package/dist/lib/spec-synthesizer.js.map +1 -1
  80. package/dist/runtime/app-create-orchestrator.d.ts +5 -1
  81. package/dist/runtime/app-create-orchestrator.js +114 -39
  82. package/dist/runtime/app-create-orchestrator.js.map +1 -1
  83. package/dist/runtime/check-catalog.js +5 -3
  84. package/dist/runtime/check-catalog.js.map +1 -1
  85. package/dist/runtime/check-definitions.d.ts +10 -0
  86. package/dist/runtime/check-definitions.js +52 -2
  87. package/dist/runtime/check-definitions.js.map +1 -1
  88. package/dist/runtime/composition-verifier.js +8 -12
  89. package/dist/runtime/composition-verifier.js.map +1 -1
  90. package/dist/runtime/gap-detector.js +8 -10
  91. package/dist/runtime/gap-detector.js.map +1 -1
  92. package/dist/runtime/input-validator.d.ts +7 -0
  93. package/dist/runtime/input-validator.js +162 -0
  94. package/dist/runtime/input-validator.js.map +1 -0
  95. package/dist/runtime/model-router.d.ts +10 -0
  96. package/dist/runtime/model-router.js +42 -0
  97. package/dist/runtime/model-router.js.map +1 -0
  98. package/dist/runtime/pattern-extractor.js +8 -10
  99. package/dist/runtime/pattern-extractor.js.map +1 -1
  100. package/dist/runtime/planner.js +11 -16
  101. package/dist/runtime/planner.js.map +1 -1
  102. package/dist/runtime/prompt-guard.d.ts +2 -0
  103. package/dist/runtime/prompt-guard.js +180 -0
  104. package/dist/runtime/prompt-guard.js.map +1 -0
  105. package/dist/runtime/prompt-safety-analyzer.js +8 -13
  106. package/dist/runtime/prompt-safety-analyzer.js.map +1 -1
  107. package/dist/runtime/reasoner.js +19 -33
  108. package/dist/runtime/reasoner.js.map +1 -1
  109. package/dist/runtime/rule-meta-verifier.js +9 -11
  110. package/dist/runtime/rule-meta-verifier.js.map +1 -1
  111. package/dist/runtime/safe-executor.d.ts +23 -0
  112. package/dist/runtime/safe-executor.js +151 -0
  113. package/dist/runtime/safe-executor.js.map +1 -0
  114. package/dist/runtime/specialized-agent.js +10 -14
  115. package/dist/runtime/specialized-agent.js.map +1 -1
  116. package/dist/runtime/strategy-library.js +8 -10
  117. package/dist/runtime/strategy-library.js.map +1 -1
  118. package/dist/runtime/supabase-experience-store.js.map +1 -1
  119. package/dist/runtime/supabase-provisioner.d.ts +35 -0
  120. package/dist/runtime/supabase-provisioner.js +192 -0
  121. package/dist/runtime/supabase-provisioner.js.map +1 -0
  122. package/dist/runtime/types.d.ts +88 -0
  123. package/dist/sdk/forward-verify.js +16 -33
  124. package/dist/sdk/forward-verify.js.map +1 -1
  125. package/package.json +1 -1
  126. package/demo/data/demo-events.json +0 -103
  127. package/demo/js/demo-mode.js +0 -107
  128. package/demo/js/orb.js +0 -634
  129. package/demo/js/question-cards.js +0 -207
  130. package/demo/js/voice.js +0 -154
@@ -1,394 +1,80 @@
1
- // timeline.js — Right panel phase steps + metrics + timing
2
- // Subscribes to AppState events, renders phase progression, verification results,
3
- // global elapsed clock, per-phase durations, and heartbeat feedback
1
+ // timeline.js — Compact status bar updates (replaces old timeline panel)
2
+ // Updates the bottom status bar with phase, elapsed time, and task counts.
4
3
 
5
- const PHASES = [
6
- { id: 'idle', name: 'Idle' },
7
- { id: 'initializing', name: 'Initializing' },
8
- { id: 'plan_questioning', name: 'Refining Requirements' },
9
- { id: 'plan_refining', name: 'Updating Requirements' },
10
- { id: 'planning', name: 'Planning Architecture' },
11
- { id: 'verifying_plan', name: 'Verifying Plan' },
12
- { id: 'requirements_refining', name: 'Reviewing Plan' },
13
- { id: 'scaffolding', name: 'Scaffolding Project' },
14
- { id: 'building_feature', name: 'Building Features' },
15
- { id: 'build_verifying', name: 'Verifying Code' },
16
- { id: 'build_repairing', name: 'Repairing Build' },
17
- { id: 'integration_verifying', name: 'Integration Verification' },
18
- { id: 'functional_testing', name: 'Functional Testing' },
19
- { id: 'functional_repairing', name: 'Repairing Failures' },
20
- { id: 'cross_verifying', name: 'Cross-Verifying' },
21
- { id: 'finalizing', name: 'Finalizing' },
22
- { id: 'completed', name: 'Completed' },
23
- { id: 'failed', name: 'Failed' }
24
- ];
4
+ const AppState = window.AppState;
25
5
 
26
- // Phases that map to a canonical phase for display
27
- const PHASE_ALIASES = {
28
- 'building_wave': 'building_feature',
29
- 'awaiting_approval': 'requirements_refining',
30
- };
6
+ let clockInterval = null;
7
+ let startTime = null;
31
8
 
32
- // Track per-phase data
33
- const phaseData = {};
34
-
35
- // ── Timing state ──
36
- let globalStartTime = null;
37
- let globalClockInterval = null;
38
- let activePhaseClockInterval = null;
39
- const phaseStartTimes = {}; // phaseId → timestamp
40
- const phaseDurations = {}; // phaseId → seconds
41
- let currentActivePhase = 'idle';
42
-
43
- // ── Heartbeat state ──
44
- let lastEventTime = null;
45
- let heartbeatInterval = null;
46
- const HEARTBEAT_THRESHOLD_MS = 4000; // show heartbeat after 4s of silence
47
-
48
- function resetPhaseData() {
49
- for (const p of PHASES) {
50
- phaseData[p.id] = { tasks: [], verifications: [] };
51
- }
52
- }
53
-
54
- function resetTimingState() {
55
- globalStartTime = null;
56
- if (globalClockInterval) clearInterval(globalClockInterval);
57
- if (activePhaseClockInterval) clearInterval(activePhaseClockInterval);
58
- if (heartbeatInterval) clearInterval(heartbeatInterval);
59
- globalClockInterval = null;
60
- activePhaseClockInterval = null;
61
- heartbeatInterval = null;
62
- lastEventTime = null;
63
- currentActivePhase = 'idle';
64
- for (const key of Object.keys(phaseStartTimes)) delete phaseStartTimes[key];
65
- for (const key of Object.keys(phaseDurations)) delete phaseDurations[key];
66
-
67
- // Reset clock display
68
- const clockEl = document.getElementById('global-clock');
69
- const clockVal = document.getElementById('clock-value');
70
- if (clockEl) clockEl.classList.add('hidden');
71
- if (clockVal) clockVal.textContent = '00:00';
72
- }
73
-
74
- function resolvePhase(phaseId) {
75
- return PHASE_ALIASES[phaseId] || phaseId;
76
- }
77
-
78
- function getPhaseIndex(phaseId) {
79
- const resolved = resolvePhase(phaseId);
80
- return PHASES.findIndex(p => p.id === resolved);
81
- }
82
-
83
- // ── Time formatting ──
84
-
85
- function formatElapsed(ms) {
86
- const totalSec = Math.floor(ms / 1000);
87
- const min = Math.floor(totalSec / 60);
88
- const sec = totalSec % 60;
89
- const tenths = Math.floor((ms % 1000) / 100);
90
- if (min > 0) {
91
- return `${min}:${sec.toString().padStart(2, '0')}.${tenths}`;
92
- }
93
- return `${sec}.${tenths}s`;
94
- }
95
-
96
- function formatClockDisplay(ms) {
97
- const totalSec = Math.floor(ms / 1000);
98
- const min = Math.floor(totalSec / 60);
99
- const sec = totalSec % 60;
100
- return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
101
- }
102
-
103
- // ── Global clock ──
104
-
105
- function startGlobalClock() {
106
- if (globalStartTime) return; // already running
107
- globalStartTime = Date.now();
108
- const clockEl = document.getElementById('global-clock');
109
- const clockVal = document.getElementById('clock-value');
110
- if (clockEl) clockEl.classList.remove('hidden');
111
-
112
- globalClockInterval = setInterval(() => {
113
- if (!globalStartTime) return;
114
- const elapsed = Date.now() - globalStartTime;
115
- if (clockVal) clockVal.textContent = formatClockDisplay(elapsed);
116
- }, 100);
117
- }
118
-
119
- function stopGlobalClock() {
120
- // Don't clear globalStartTime — keep final time displayed
121
- if (globalClockInterval) {
122
- clearInterval(globalClockInterval);
123
- globalClockInterval = null;
124
- }
125
- }
126
-
127
- // ── Phase timing ──
128
-
129
- function onPhaseEnter(phaseId) {
130
- const resolved = resolvePhase(phaseId);
131
-
132
- // Start global clock on first non-idle phase
133
- if (resolved !== 'idle') {
134
- startGlobalClock();
135
- }
136
-
137
- // Record phase start
138
- phaseStartTimes[resolved] = Date.now();
139
- currentActivePhase = resolved;
140
- lastEventTime = Date.now();
141
-
142
- // Start active phase clock (updates the active step's elapsed badge)
143
- if (activePhaseClockInterval) clearInterval(activePhaseClockInterval);
144
- activePhaseClockInterval = setInterval(() => {
145
- updateActivePhaseElapsed();
146
- updateHeartbeat();
147
- }, 100);
148
-
149
- // Stop global clock on terminal phases
150
- if (resolved === 'completed' || resolved === 'failed') {
151
- stopGlobalClock();
152
- // Record final duration for the terminal phase itself
153
- phaseDurations[resolved] = 0;
154
- if (activePhaseClockInterval) {
155
- clearInterval(activePhaseClockInterval);
156
- activePhaseClockInterval = null;
157
- }
158
- if (heartbeatInterval) {
159
- clearInterval(heartbeatInterval);
160
- heartbeatInterval = null;
9
+ export function initTimeline() {
10
+ AppState.on('phase_change', ({ from, to }) => {
11
+ if (from === 'idle' && to !== 'idle') {
12
+ startClock();
161
13
  }
162
- }
163
- }
164
-
165
- function onPhaseExit(phaseId) {
166
- const resolved = resolvePhase(phaseId);
167
- const start = phaseStartTimes[resolved];
168
- if (start) {
169
- phaseDurations[resolved] = (Date.now() - start) / 1000;
170
- }
171
- }
172
-
173
- function updateActivePhaseElapsed() {
174
- const resolved = currentActivePhase;
175
- const start = phaseStartTimes[resolved];
176
- if (!start) return;
177
-
178
- const elapsedEl = document.querySelector('.phase-elapsed-live');
179
- if (elapsedEl) {
180
- const elapsed = Date.now() - start;
181
- elapsedEl.textContent = formatElapsed(elapsed);
182
- }
183
- }
184
-
185
- // ── Heartbeat ──
186
-
187
- function recordEvent() {
188
- lastEventTime = Date.now();
189
- // Hide heartbeat immediately when event arrives
190
- const hb = document.querySelector('.heartbeat-indicator');
191
- if (hb) hb.classList.remove('visible');
192
- }
193
-
194
- function updateHeartbeat() {
195
- if (!lastEventTime) return;
196
- const silence = Date.now() - lastEventTime;
197
- const hb = document.querySelector('.heartbeat-indicator');
198
- if (!hb) return;
199
-
200
- if (silence > HEARTBEAT_THRESHOLD_MS) {
201
- hb.classList.add('visible');
202
- } else {
203
- hb.classList.remove('visible');
204
- }
205
- }
206
-
207
- // ── Render ──
208
14
 
209
- function renderSteps(currentPhase) {
210
- const container = document.getElementById('timeline-steps');
211
- if (!container) return;
212
-
213
- const currentIdx = getPhaseIndex(currentPhase);
214
-
215
- container.innerHTML = '';
216
-
217
- for (let i = 0; i < PHASES.length; i++) {
218
- const phase = PHASES[i];
219
- const step = document.createElement('div');
220
- step.className = 'timeline-step';
221
- step.dataset.phase = phase.id;
222
-
223
- if (i < currentIdx) {
224
- step.classList.add('completed');
225
- } else if (i === currentIdx) {
226
- step.classList.add('active');
227
- } else {
228
- step.classList.add('pending');
15
+ const phaseEl = document.getElementById('status-phase');
16
+ if (phaseEl) {
17
+ phaseEl.textContent = `phase: ${formatPhase(to)}`;
229
18
  }
230
19
 
231
- // Step header row: name + duration
232
- const headerRow = document.createElement('div');
233
- headerRow.className = 'step-header';
234
-
235
- const nameEl = document.createElement('div');
236
- nameEl.className = 'step-name';
237
- nameEl.textContent = phase.name;
238
- headerRow.appendChild(nameEl);
239
-
240
- // Duration badge for completed phases
241
- if (i < currentIdx && phaseDurations[phase.id] !== undefined) {
242
- const dur = phaseDurations[phase.id];
243
- const durEl = document.createElement('span');
244
- durEl.className = 'phase-duration';
245
- durEl.textContent = dur < 60 ? `${dur.toFixed(1)}s` : `${Math.floor(dur / 60)}m${Math.floor(dur % 60)}s`;
246
- headerRow.appendChild(durEl);
20
+ const clockEl = document.getElementById('global-clock');
21
+ if (clockEl && to !== 'idle') {
22
+ clockEl.classList.remove('hidden');
247
23
  }
248
24
 
249
- // Live elapsed for active phase
250
- if (i === currentIdx && phase.id !== 'idle' && phase.id !== 'completed' && phase.id !== 'failed') {
251
- const elapsedEl = document.createElement('span');
252
- elapsedEl.className = 'phase-elapsed-live';
253
- const start = phaseStartTimes[phase.id];
254
- if (start) {
255
- elapsedEl.textContent = formatElapsed(Date.now() - start);
256
- }
257
- headerRow.appendChild(elapsedEl);
25
+ if (to === 'completed' || to === 'failed') {
26
+ stopClock();
258
27
  }
28
+ });
259
29
 
260
- step.appendChild(headerRow);
261
-
262
- // Heartbeat indicator (only on active step)
263
- if (i === currentIdx && phase.id !== 'idle' && phase.id !== 'completed' && phase.id !== 'failed') {
264
- const hbEl = document.createElement('div');
265
- hbEl.className = 'heartbeat-indicator';
266
- hbEl.innerHTML = '<span class="heartbeat-dot"></span> working';
267
- step.appendChild(hbEl);
30
+ AppState.on('metric_update', (metrics) => {
31
+ const tasksEl = document.getElementById('status-tasks');
32
+ if (tasksEl) {
33
+ tasksEl.textContent = `tasks: ${metrics.tasksComplete}/${metrics.totalTasks}`;
268
34
  }
35
+ });
269
36
 
270
- const detailsEl = document.createElement('div');
271
- detailsEl.className = 'step-details';
272
-
273
- const data = phaseData[phase.id];
274
- if (data) {
275
- for (const task of data.tasks) {
276
- const taskEl = document.createElement('div');
277
- taskEl.className = 'step-task';
278
- const badge = document.createElement('span');
279
- badge.className = 'agent-badge';
280
- badge.textContent = task.agent || 'agent';
281
- taskEl.appendChild(badge);
282
- const label = document.createTextNode(` ${task.name || task.title || task.id}`);
283
- taskEl.appendChild(label);
284
- detailsEl.appendChild(taskEl);
285
- }
286
-
287
- for (const v of data.verifications) {
288
- const vEl = document.createElement('div');
289
- vEl.className = 'verification-item';
290
- vEl.classList.add(v.result === 'pass' ? 'pass' : 'fail');
291
-
292
- const icon = document.createTextNode(v.result === 'pass' ? '\u2713 ' : '\u2717 ');
293
- vEl.appendChild(icon);
294
-
295
- const claim = document.createTextNode(v.claim || v.message || 'claim');
296
- vEl.appendChild(claim);
37
+ AppState.on('reset', () => {
38
+ stopClock();
39
+ const phaseEl = document.getElementById('status-phase');
40
+ const elapsedEl = document.getElementById('status-elapsed');
41
+ const tasksEl = document.getElementById('status-tasks');
42
+ const clockEl = document.getElementById('global-clock');
43
+ if (phaseEl) phaseEl.textContent = 'phase: idle';
44
+ if (elapsedEl) elapsedEl.textContent = 'elapsed: 00:00';
45
+ if (tasksEl) tasksEl.textContent = 'tasks: 0/0';
46
+ if (clockEl) clockEl.classList.add('hidden');
47
+ });
48
+ }
297
49
 
298
- const methodBadge = document.createElement('span');
299
- methodBadge.className = 'method-badge';
300
- methodBadge.textContent = v.method || 'llm';
301
- vEl.appendChild(methodBadge);
50
+ function startClock() {
51
+ startTime = Date.now();
52
+ clockInterval = setInterval(() => {
53
+ const elapsed = Date.now() - startTime;
54
+ const formatted = formatTime(elapsed);
302
55
 
303
- detailsEl.appendChild(vEl);
304
- }
305
- }
56
+ const elapsedEl = document.getElementById('status-elapsed');
57
+ if (elapsedEl) elapsedEl.textContent = `elapsed: ${formatted}`;
306
58
 
307
- step.appendChild(detailsEl);
308
- container.appendChild(step);
309
- }
59
+ const clockValue = document.getElementById('clock-value');
60
+ if (clockValue) clockValue.textContent = formatted;
61
+ }, 1000);
62
+ }
310
63
 
311
- // Auto-scroll to active step
312
- const activeStep = container.querySelector('.timeline-step.active');
313
- if (activeStep) {
314
- activeStep.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
64
+ function stopClock() {
65
+ if (clockInterval) {
66
+ clearInterval(clockInterval);
67
+ clockInterval = null;
315
68
  }
316
69
  }
317
70
 
318
- function updateMetrics(metrics) {
319
- const tasks = document.getElementById('metric-tasks');
320
- const claims = document.getElementById('metric-claims');
321
- const failed = document.getElementById('metric-failed');
322
- const files = document.getElementById('metric-files');
323
-
324
- if (tasks) tasks.textContent = `${metrics.tasksComplete}/${metrics.totalTasks}`;
325
- if (claims) claims.textContent = metrics.claimsVerified;
326
- if (failed) failed.textContent = metrics.claimsFailed;
327
- if (files) files.textContent = metrics.filesGenerated;
71
+ function formatTime(ms) {
72
+ const totalSeconds = Math.floor(ms / 1000);
73
+ const minutes = Math.floor(totalSeconds / 60);
74
+ const seconds = totalSeconds % 60;
75
+ return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
328
76
  }
329
77
 
330
- export function initTimeline() {
331
- const state = window.AppState;
332
- if (!state) return;
333
-
334
- resetPhaseData();
335
- resetTimingState();
336
- renderSteps('idle');
337
- updateMetrics(state.get().metrics);
338
-
339
- state.on('phase_change', ({ from, to }) => {
340
- const resolvedFrom = resolvePhase(from);
341
- const resolvedTo = resolvePhase(to);
342
-
343
- // Record duration for outgoing phase
344
- onPhaseExit(resolvedFrom);
345
-
346
- // Start timing for incoming phase
347
- onPhaseEnter(resolvedTo);
348
-
349
- renderSteps(resolvedTo);
350
- recordEvent();
351
- });
352
-
353
- state.on('task_update', (task) => {
354
- if (!task) return;
355
- const currentPhase = resolvePhase(state.get().phase);
356
-
357
- // Handle single task
358
- if (task && task.id) {
359
- const pd = phaseData[currentPhase];
360
- if (pd && !pd.tasks.find(t => t.id === task.id)) {
361
- pd.tasks.push(task);
362
- }
363
- renderSteps(currentPhase);
364
- }
365
- recordEvent();
366
- });
367
-
368
- state.on('verification', (v) => {
369
- if (!v) return;
370
- const currentPhase = resolvePhase(state.get().phase);
371
- const pd = phaseData[currentPhase];
372
- if (pd) {
373
- pd.verifications.push(v);
374
- renderSteps(currentPhase);
375
- }
376
- recordEvent();
377
- });
378
-
379
- state.on('code_generated', () => {
380
- recordEvent();
381
- });
382
-
383
- state.on('metric_update', (metrics) => {
384
- updateMetrics(metrics);
385
- recordEvent();
386
- });
387
-
388
- state.on('reset', () => {
389
- resetPhaseData();
390
- resetTimingState();
391
- renderSteps('idle');
392
- updateMetrics({ tasksComplete: 0, totalTasks: 0, claimsVerified: 0, claimsFailed: 0, filesGenerated: 0 });
393
- });
78
+ function formatPhase(phase) {
79
+ return phase.replace(/_/g, ' ').replace(/\b\w/g, c => c);
394
80
  }
@@ -4,8 +4,10 @@ export declare class TeamApiServer {
4
4
  private appSessions;
5
5
  private server;
6
6
  private port;
7
+ private demoMode;
7
8
  constructor(opts?: {
8
9
  port?: number;
10
+ demoMode?: boolean;
9
11
  });
10
12
  /** Register an API key for testing/development. */
11
13
  registerApiKey(key: string, tier: 'team' | 'pro' | 'platform', orgId: string): void;
@@ -11,18 +11,22 @@
11
11
  // ============================================================
12
12
  import { createServer } from 'node:http';
13
13
  import { randomUUID } from 'node:crypto';
14
- import { spawn } from 'node:child_process';
15
14
  import { PricingEnforcer } from './pricing-enforcer.js';
16
15
  import { TeamSessionManager } from './team-session.js';
17
16
  import { AppCreateOrchestrator } from '../runtime/app-create-orchestrator.js';
17
+ import { validateAppDescription } from '../runtime/input-validator.js';
18
+ import { scanForInjection } from '../runtime/prompt-guard.js';
19
+ import { safeSpawn } from '../runtime/safe-executor.js';
18
20
  export class TeamApiServer {
19
21
  pricingEnforcer;
20
22
  sessionManager;
21
23
  appSessions = new Map();
22
24
  server = null;
23
25
  port;
26
+ demoMode;
24
27
  constructor(opts) {
25
28
  this.port = opts?.port ?? 3800;
29
+ this.demoMode = opts?.demoMode ?? false;
26
30
  this.pricingEnforcer = new PricingEnforcer();
27
31
  this.sessionManager = new TeamSessionManager();
28
32
  }
@@ -72,15 +76,17 @@ export class TeamApiServer {
72
76
  res.end();
73
77
  return;
74
78
  }
75
- // Authenticate
79
+ // Authenticate (skip in demo mode)
76
80
  const apiKey = this.extractApiKey(req);
77
- if (!apiKey && path !== '/health') {
78
- this.sendError(res, 401, 'Missing API key. Include Authorization: Bearer <key>');
79
- return;
80
- }
81
- if (apiKey && !this.pricingEnforcer.isValidKey(apiKey) && path !== '/health') {
82
- this.sendError(res, 403, 'Invalid API key');
83
- return;
81
+ if (!this.demoMode) {
82
+ if (!apiKey && path !== '/health') {
83
+ this.sendError(res, 401, 'Missing API key. Include Authorization: Bearer <key>');
84
+ return;
85
+ }
86
+ if (apiKey && !this.pricingEnforcer.isValidKey(apiKey) && path !== '/health') {
87
+ this.sendError(res, 403, 'Invalid API key');
88
+ return;
89
+ }
84
90
  }
85
91
  // Route
86
92
  if (path === '/health' && method === 'GET') {
@@ -283,26 +289,41 @@ export class TeamApiServer {
283
289
  // ── App Creation Handlers ────────────────────────────────
284
290
  async handleAppCreate(req, res, _apiKey) {
285
291
  const body = await this.readBody(req);
286
- let request;
292
+ let parsed;
287
293
  try {
288
- request = JSON.parse(body);
294
+ parsed = JSON.parse(body);
289
295
  }
290
296
  catch {
291
297
  this.sendError(res, 400, 'Invalid JSON body');
292
298
  return;
293
299
  }
294
- if (!request.name || !request.description || !request.techStack) {
295
- this.sendError(res, 400, 'Missing required fields: name, description, techStack');
300
+ // Validate and sanitize input at the trust boundary
301
+ const validation = validateAppDescription(parsed);
302
+ if (!validation.valid) {
303
+ this.sendError(res, 400, `Validation: ${validation.errors.map(e => `${e.field}: ${e.message}`).join('; ')}`);
296
304
  return;
297
305
  }
306
+ const request = validation.sanitized;
307
+ // Scan for prompt injection in user-provided text
308
+ const descGuard = scanForInjection(request.description);
309
+ if (descGuard.action === 'block') {
310
+ this.sendError(res, 400, `Request blocked: suspicious content detected in description (score: ${descGuard.score.toFixed(2)})`);
311
+ return;
312
+ }
313
+ if (descGuard.action === 'warn') {
314
+ console.warn(`[prompt-guard] Warning on app description (score: ${descGuard.score.toFixed(2)}):`, descGuard.findings);
315
+ }
298
316
  // Parse optional flags from request body
299
- const bodyParsed = JSON.parse(body);
317
+ const bodyParsed = parsed;
300
318
  const sessionId = randomUUID();
301
319
  const orchestrator = new AppCreateOrchestrator(request, {
302
320
  outputPath: process.cwd(),
303
321
  autoApprove: bodyParsed.autoApprove,
304
322
  skipFunctionalTesting: bodyParsed.skipFunctionalTesting,
305
323
  skipPlanQuestions: bodyParsed.skipPlanQuestions,
324
+ supabaseOrgId: process.env.SUPABASE_ORG_ID,
325
+ supabaseRegion: process.env.SUPABASE_REGION,
326
+ documents: bodyParsed.documents,
306
327
  });
307
328
  const session = {
308
329
  id: sessionId,
@@ -563,6 +584,16 @@ export class TeamApiServer {
563
584
  this.sendError(res, 400, 'Missing required field: answers (array)');
564
585
  return;
565
586
  }
587
+ // Scan custom text answers for prompt injection
588
+ for (const answer of parsed.answers) {
589
+ if (answer.customText) {
590
+ const answerGuard = scanForInjection(answer.customText);
591
+ if (answerGuard.action === 'block') {
592
+ this.sendError(res, 400, `Answer blocked: suspicious content in custom text (score: ${answerGuard.score.toFixed(2)})`);
593
+ return;
594
+ }
595
+ }
596
+ }
566
597
  session.orchestrator.submitAnswers(parsed.answers);
567
598
  this.sendJson(res, 200, { status: 'received' });
568
599
  }
@@ -589,7 +620,22 @@ export class TeamApiServer {
589
620
  this.sendError(res, 400, 'Missing required field: message (string)');
590
621
  return;
591
622
  }
592
- session.orchestrator.submitChatMessage(parsed.message, parsed.cardAnswers);
623
+ // Scan chat message for prompt injection
624
+ const chatGuard = scanForInjection(parsed.message);
625
+ if (chatGuard.action === 'block') {
626
+ this.sendError(res, 400, `Message blocked: suspicious content detected (score: ${chatGuard.score.toFixed(2)})`);
627
+ return;
628
+ }
629
+ if (chatGuard.action === 'warn') {
630
+ console.warn(`[prompt-guard] Warning on chat message (score: ${chatGuard.score.toFixed(2)}):`, chatGuard.findings);
631
+ }
632
+ // Append document contents to message if provided
633
+ let messageWithDocs = parsed.message;
634
+ if (parsed.documents && parsed.documents.length > 0) {
635
+ const docText = parsed.documents.map(d => `\n\n--- Attached: ${d.name} ---\n${d.content}`).join('');
636
+ messageWithDocs = parsed.message + docText;
637
+ }
638
+ session.orchestrator.submitChatMessage(messageWithDocs, parsed.cardAnswers);
593
639
  this.sendJson(res, 200, { status: 'received' });
594
640
  }
595
641
  // ── App Launch Handlers ─────────────────────────────────
@@ -613,9 +659,8 @@ export class TeamApiServer {
613
659
  }
614
660
  const projectPath = session.result.projectPath;
615
661
  session.launchStatus = 'installing';
616
- const install = spawn('npm', ['install'], {
662
+ const install = safeSpawn('npm', ['install'], {
617
663
  cwd: projectPath,
618
- shell: true,
619
664
  stdio: ['ignore', 'pipe', 'pipe'],
620
665
  });
621
666
  install.on('close', (code) => {
@@ -625,9 +670,8 @@ export class TeamApiServer {
625
670
  return;
626
671
  }
627
672
  session.launchStatus = 'starting';
628
- const dev = spawn('npm', ['run', 'dev'], {
673
+ const dev = safeSpawn('npm', ['run', 'dev'], {
629
674
  cwd: projectPath,
630
- shell: true,
631
675
  stdio: ['ignore', 'pipe', 'pipe'],
632
676
  });
633
677
  session.launchProcess = dev;