agent-state-machine 2.0.15 → 2.1.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 (47) hide show
  1. package/bin/cli.js +1 -1
  2. package/lib/index.js +33 -0
  3. package/lib/remote/client.js +7 -2
  4. package/lib/runtime/agent.js +102 -67
  5. package/lib/runtime/index.js +13 -0
  6. package/lib/runtime/interaction.js +304 -0
  7. package/lib/runtime/prompt.js +39 -12
  8. package/lib/runtime/runtime.js +11 -10
  9. package/package.json +1 -1
  10. package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
  11. package/templates/project-builder/agents/code-reviewer.md +0 -1
  12. package/templates/project-builder/agents/code-writer.md +0 -1
  13. package/templates/project-builder/agents/requirements-clarifier.md +0 -1
  14. package/templates/project-builder/agents/response-interpreter.md +25 -0
  15. package/templates/project-builder/agents/roadmap-generator.md +0 -1
  16. package/templates/project-builder/agents/sanity-checker.md +45 -0
  17. package/templates/project-builder/agents/sanity-runner.js +161 -0
  18. package/templates/project-builder/agents/scope-clarifier.md +0 -1
  19. package/templates/project-builder/agents/security-clarifier.md +0 -1
  20. package/templates/project-builder/agents/security-reviewer.md +0 -1
  21. package/templates/project-builder/agents/task-planner.md +0 -1
  22. package/templates/project-builder/agents/test-planner.md +0 -1
  23. package/templates/project-builder/scripts/interaction-helpers.js +33 -0
  24. package/templates/project-builder/scripts/workflow-helpers.js +2 -47
  25. package/templates/project-builder/workflow.js +214 -54
  26. package/vercel-server/api/session/[token].js +3 -3
  27. package/vercel-server/api/submit/[token].js +5 -3
  28. package/vercel-server/local-server.js +33 -6
  29. package/vercel-server/public/remote/index.html +17 -0
  30. package/vercel-server/ui/index.html +9 -1012
  31. package/vercel-server/ui/package-lock.json +2650 -0
  32. package/vercel-server/ui/package.json +25 -0
  33. package/vercel-server/ui/postcss.config.js +6 -0
  34. package/vercel-server/ui/src/App.jsx +236 -0
  35. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
  36. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
  37. package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
  38. package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
  39. package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
  40. package/vercel-server/ui/src/components/Footer.jsx +66 -0
  41. package/vercel-server/ui/src/components/Header.jsx +38 -0
  42. package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
  43. package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
  44. package/vercel-server/ui/src/index.css +145 -0
  45. package/vercel-server/ui/src/main.jsx +8 -0
  46. package/vercel-server/ui/tailwind.config.js +19 -0
  47. package/vercel-server/ui/vite.config.js +11 -0
@@ -8,7 +8,7 @@
8
8
  * 4. Task lifecycle with optimal agent sequencing
9
9
  */
10
10
 
11
- import { memory, askHuman } from 'agent-state-machine';
11
+ import { agent, memory, askHuman } from 'agent-state-machine';
12
12
  import path from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import {
@@ -16,13 +16,17 @@ import {
16
16
  isApproval,
17
17
  renderRoadmapMarkdown,
18
18
  renderTasksMarkdown,
19
- safeAgent,
20
19
  TASK_STAGES,
21
20
  getTaskStage,
22
21
  setTaskStage,
23
22
  getTaskData,
24
23
  setTaskData
25
24
  } from './scripts/workflow-helpers.js';
25
+ import {
26
+ createInteraction,
27
+ parseResponse,
28
+ formatInteractionPrompt as formatPrompt
29
+ } from './scripts/interaction-helpers.js';
26
30
 
27
31
  // Derive workflow directory dynamically
28
32
  const __filename = fileURLToPath(import.meta.url);
@@ -43,11 +47,17 @@ export default async function () {
43
47
  console.log('=== PHASE 1: PROJECT INTAKE ===\n');
44
48
 
45
49
  if (!memory.projectDescription) {
46
- const description = await askHuman(
47
- 'Describe the project you want to build. Include any initial requirements, goals, or constraints you have in mind.',
48
- { slug: 'project-description' }
49
- );
50
- memory.projectDescription = description;
50
+ const descriptionInteraction = createInteraction('text', 'project-description', {
51
+ prompt: 'Describe the project you want to build. Include any initial requirements, goals, or constraints you have in mind.',
52
+ placeholder: 'A web app that...',
53
+ validation: { minLength: 20 }
54
+ });
55
+ const descriptionRaw = await askHuman(formatPrompt(descriptionInteraction), {
56
+ slug: descriptionInteraction.slug,
57
+ interaction: descriptionInteraction
58
+ });
59
+ const descriptionParsed = await parseResponse(descriptionInteraction, descriptionRaw);
60
+ memory.projectDescription = descriptionParsed.text || descriptionParsed.raw || descriptionRaw;
51
61
  }
52
62
 
53
63
  console.log('Project description captured. Starting clarification process...\n');
@@ -61,7 +71,7 @@ export default async function () {
61
71
  // 1. Scope Clarification
62
72
  if (!memory.scopeClarified) {
63
73
  console.log('--- Scope Clarification ---');
64
- const scopeResult = await safeAgent('scope-clarifier', {
74
+ const scopeResult = await agent('scope-clarifier', {
65
75
  projectDescription: memory.projectDescription
66
76
  });
67
77
  memory.scope = scopeResult;
@@ -71,7 +81,7 @@ export default async function () {
71
81
  // 2. Requirements Clarification
72
82
  if (!memory.requirementsClarified) {
73
83
  console.log('--- Requirements Clarification ---');
74
- const reqResult = await safeAgent('requirements-clarifier', {
84
+ const reqResult = await agent('requirements-clarifier', {
75
85
  projectDescription: memory.projectDescription,
76
86
  scope: memory.scope
77
87
  });
@@ -82,7 +92,7 @@ export default async function () {
82
92
  // 3. Assumptions Clarification
83
93
  if (!memory.assumptionsClarified) {
84
94
  console.log('--- Assumptions Clarification ---');
85
- const assumeResult = await safeAgent('assumptions-clarifier', {
95
+ const assumeResult = await agent('assumptions-clarifier', {
86
96
  projectDescription: memory.projectDescription,
87
97
  scope: memory.scope,
88
98
  requirements: memory.requirements
@@ -94,7 +104,7 @@ export default async function () {
94
104
  // 4. Security Clarification
95
105
  if (!memory.securityClarified) {
96
106
  console.log('--- Security Clarification ---');
97
- const secResult = await safeAgent('security-clarifier', {
107
+ const secResult = await agent('security-clarifier', {
98
108
  projectDescription: memory.projectDescription,
99
109
  scope: memory.scope,
100
110
  requirements: memory.requirements,
@@ -114,7 +124,7 @@ export default async function () {
114
124
  if (!memory.roadmapApproved) {
115
125
  // Generate roadmap as JSON
116
126
  if (!memory.roadmap) {
117
- const roadmapResult = await safeAgent('roadmap-generator', {
127
+ const roadmapResult = await agent('roadmap-generator', {
118
128
  projectDescription: memory.projectDescription,
119
129
  scope: memory.scope,
120
130
  requirements: memory.requirements,
@@ -129,24 +139,35 @@ export default async function () {
129
139
  // Roadmap approval loop
130
140
  let approved = false;
131
141
  while (!approved) {
132
- const reviewResponse = await askHuman(
133
- `Please review the roadmap in state/roadmap.md\n\nOptions:\n- A: Approve roadmap as-is\n- B: Request changes (describe what to change)\n\nYour choice:`,
134
- { slug: 'roadmap-review' }
135
- );
142
+ const roadmapInteraction = createInteraction('choice', 'roadmap-review', {
143
+ prompt: 'Please review the roadmap in state/roadmap.md.\nHow would you like to proceed?',
144
+ options: [
145
+ { key: 'approve', label: 'Approve roadmap as-is' },
146
+ { key: 'changes', label: 'Request changes', description: 'Describe what to change' }
147
+ ],
148
+ allowCustom: true
149
+ });
150
+
151
+ const reviewRaw = await askHuman(formatPrompt(roadmapInteraction), {
152
+ slug: roadmapInteraction.slug,
153
+ interaction: roadmapInteraction
154
+ });
155
+ const reviewResponse = await parseResponse(roadmapInteraction, reviewRaw);
136
156
 
137
- if (isApproval(reviewResponse)) {
157
+ if (reviewResponse.selectedKey === 'approve' || isApproval(reviewResponse.raw || reviewRaw)) {
138
158
  approved = true;
139
159
  memory.roadmapApproved = true;
140
160
  console.log('Roadmap approved!\n');
141
161
  } else {
162
+ const feedback = reviewResponse.customText || reviewResponse.text || reviewResponse.raw || reviewRaw;
142
163
  // Regenerate roadmap with feedback
143
- const updatedRoadmap = await safeAgent('roadmap-generator', {
164
+ const updatedRoadmap = await agent('roadmap-generator', {
144
165
  projectDescription: memory.projectDescription,
145
166
  scope: memory.scope,
146
167
  requirements: memory.requirements,
147
168
  assumptions: memory.assumptions,
148
169
  security: memory.security,
149
- feedback: reviewResponse
170
+ feedback
150
171
  });
151
172
  memory.roadmap = updatedRoadmap;
152
173
  writeMarkdownFile(STATE_DIR, 'roadmap.md', renderRoadmapMarkdown(memory.roadmap));
@@ -178,7 +199,7 @@ export default async function () {
178
199
  // Generate task list for this phase (as JSON)
179
200
  if (!memory[tasksApprovedKey]) {
180
201
  if (!memory[tasksKey]) {
181
- const taskResult = await safeAgent('task-planner', {
202
+ const taskResult = await agent('task-planner', {
182
203
  projectDescription: memory.projectDescription,
183
204
  scope: memory.scope,
184
205
  requirements: memory.requirements,
@@ -193,23 +214,34 @@ export default async function () {
193
214
  // Task list approval loop
194
215
  let tasksApproved = false;
195
216
  while (!tasksApproved) {
196
- const taskReview = await askHuman(
197
- `Please review the task list for Phase ${i + 1} in state/phase-${i + 1}-tasks.md\n\nOptions:\n- A: Approve task list\n- B: Request changes (describe what to change)\n\nYour choice:`,
198
- { slug: `phase-${i + 1}-task-review` }
199
- );
217
+ const taskReviewInteraction = createInteraction('choice', `phase-${i + 1}-task-review`, {
218
+ prompt: `Please review the task list for Phase ${i + 1} in state/phase-${i + 1}-tasks.md.\nHow would you like to proceed?`,
219
+ options: [
220
+ { key: 'approve', label: 'Approve task list' },
221
+ { key: 'changes', label: 'Request changes', description: 'Describe what to change' }
222
+ ],
223
+ allowCustom: true
224
+ });
200
225
 
201
- if (isApproval(taskReview)) {
226
+ const taskReviewRaw = await askHuman(formatPrompt(taskReviewInteraction), {
227
+ slug: taskReviewInteraction.slug,
228
+ interaction: taskReviewInteraction
229
+ });
230
+ const taskReview = await parseResponse(taskReviewInteraction, taskReviewRaw);
231
+
232
+ if (taskReview.selectedKey === 'approve' || isApproval(taskReview.raw || taskReviewRaw)) {
202
233
  tasksApproved = true;
203
234
  memory[tasksApprovedKey] = true;
204
235
  console.log(`Phase ${i + 1} task list approved!\n`);
205
236
  } else {
206
- const updatedTasks = await safeAgent('task-planner', {
237
+ const feedback = taskReview.customText || taskReview.text || taskReview.raw || taskReviewRaw;
238
+ const updatedTasks = await agent('task-planner', {
207
239
  projectDescription: memory.projectDescription,
208
240
  scope: memory.scope,
209
241
  requirements: memory.requirements,
210
242
  phase: phase,
211
243
  phaseIndex: i + 1,
212
- feedback: taskReview
244
+ feedback
213
245
  });
214
246
  memory[tasksKey] = updatedTasks;
215
247
  writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, memory[tasksKey]?.tasks || memory[tasksKey]));
@@ -241,6 +273,15 @@ export default async function () {
241
273
  // Get current stage for this task
242
274
  let stage = getTaskStage(i, taskId);
243
275
 
276
+ // Update progress tracking for remote monitoring
277
+ memory.progress = {
278
+ phase: `${i + 1}/${phases.length}`,
279
+ task: `${t + 1}/${tasks.length}`,
280
+ stage: stage,
281
+ currentTask: task.title,
282
+ currentPhase: phase.title
283
+ };
284
+
244
285
  // Store any feedback for this task
245
286
  const feedback = getTaskData(i, taskId, 'feedback');
246
287
 
@@ -249,7 +290,7 @@ export default async function () {
249
290
  if (stage === TASK_STAGES.PENDING || stage === TASK_STAGES.SECURITY_PRE) {
250
291
  if (!getTaskData(i, taskId, 'security_pre')) {
251
292
  console.log(' > Security pre-review...');
252
- const securityPreReview = await safeAgent('security-reviewer', {
293
+ const securityPreReview = await agent('security-reviewer', {
253
294
  task: task,
254
295
  phase: phase,
255
296
  scope: memory.scope,
@@ -266,7 +307,7 @@ export default async function () {
266
307
  if (stage === TASK_STAGES.TEST_PLANNING) {
267
308
  if (!getTaskData(i, taskId, 'tests')) {
268
309
  console.log(' > Test planning...');
269
- const testPlan = await safeAgent('test-planner', {
310
+ const testPlan = await agent('test-planner', {
270
311
  task: task,
271
312
  phase: phase,
272
313
  requirements: memory.requirements,
@@ -283,7 +324,7 @@ export default async function () {
283
324
  if (stage === TASK_STAGES.IMPLEMENTING) {
284
325
  if (!getTaskData(i, taskId, 'code')) {
285
326
  console.log(' > Code implementation...');
286
- const implementation = await safeAgent('code-writer', {
327
+ const implementation = await agent('code-writer', {
287
328
  task: task,
288
329
  phase: phase,
289
330
  requirements: memory.requirements,
@@ -301,7 +342,7 @@ export default async function () {
301
342
  if (stage === TASK_STAGES.CODE_REVIEW) {
302
343
  if (!getTaskData(i, taskId, 'review')) {
303
344
  console.log(' > Code review...');
304
- const codeReview = await safeAgent('code-reviewer', {
345
+ const codeReview = await agent('code-reviewer', {
305
346
  task: task,
306
347
  implementation: getTaskData(i, taskId, 'code'),
307
348
  testPlan: getTaskData(i, taskId, 'tests'),
@@ -317,7 +358,7 @@ export default async function () {
317
358
  if (stage === TASK_STAGES.SECURITY_POST) {
318
359
  if (!getTaskData(i, taskId, 'security_post')) {
319
360
  console.log(' > Final security check...');
320
- const securityPostReview = await safeAgent('security-reviewer', {
361
+ const securityPostReview = await agent('security-reviewer', {
321
362
  task: task,
322
363
  phase: phase,
323
364
  implementation: getTaskData(i, taskId, 'code'),
@@ -326,39 +367,147 @@ export default async function () {
326
367
  });
327
368
  setTaskData(i, taskId, 'security_post', securityPostReview);
328
369
  }
329
- setTaskStage(i, taskId, TASK_STAGES.AWAITING_APPROVAL);
330
- stage = TASK_STAGES.AWAITING_APPROVAL;
370
+ setTaskStage(i, taskId, TASK_STAGES.SANITY_CHECK);
371
+ stage = TASK_STAGES.SANITY_CHECK;
331
372
  }
332
373
 
333
- // 6. Sanity check with user
374
+ // 6. Sanity check generation & execution
375
+ if (stage === TASK_STAGES.SANITY_CHECK) {
376
+ const executableChecks = await agent('sanity-checker', {
377
+ task: task,
378
+ implementation: getTaskData(i, taskId, 'code'),
379
+ testPlan: getTaskData(i, taskId, 'tests')
380
+ });
381
+ setTaskData(i, taskId, 'sanity_checks', executableChecks);
382
+
383
+ const checksDisplay = (executableChecks.checks || [])
384
+ .map((check) => ` ${check.id}. ${check.description}\n → ${check.command || check.path || check.testCommand}`)
385
+ .join('\n');
386
+
387
+ const sanityChoice = createInteraction('choice', `phase-${i + 1}-task-${taskId}-sanity-choice`, {
388
+ prompt: `Sanity checks for "${task.title}":\n\n${checksDisplay}\n\nHow would you like to proceed?`,
389
+ options: [
390
+ { key: 'manual', label: 'Run checks manually', description: 'You run the commands and confirm results' },
391
+ { key: 'auto', label: 'Run automatically', description: 'Agent executes checks and reports results' },
392
+ { key: 'skip', label: 'Skip verification', description: 'Approve without running checks' }
393
+ ],
394
+ allowCustom: true
395
+ });
396
+
397
+ const sanityRaw = await askHuman(formatPrompt(sanityChoice), {
398
+ slug: sanityChoice.slug,
399
+ interaction: sanityChoice
400
+ });
401
+ const sanityResponse = await parseResponse(sanityChoice, sanityRaw);
402
+
403
+ if (sanityResponse.isCustom) {
404
+ setTaskData(i, taskId, 'feedback', sanityResponse.customText || sanityResponse.raw || sanityRaw);
405
+ setTaskStage(i, taskId, TASK_STAGES.PENDING);
406
+ t--;
407
+ continue;
408
+ }
409
+
410
+ const action = sanityResponse.selectedKey;
411
+
412
+ if (action === 'auto') {
413
+ const results = await agent('sanity-runner', {
414
+ checks: executableChecks.checks,
415
+ setup: executableChecks.setup,
416
+ teardown: executableChecks.teardown
417
+ });
418
+ setTaskData(i, taskId, 'sanity_results', results);
419
+
420
+ if (results.summary?.failed > 0) {
421
+ const failedChecks = results.results
422
+ .filter((r) => r.status === 'failed')
423
+ .map((r) => ` - Check ${r.id}: ${r.error}`)
424
+ .join('\n');
425
+
426
+ const failChoice = createInteraction('choice', `phase-${i + 1}-task-${taskId}-sanity-fail`, {
427
+ prompt: `${results.summary.failed} sanity check(s) failed:\n\n${failedChecks}\n\nHow would you like to proceed?`,
428
+ options: [
429
+ { key: 'reimplement', label: 'Re-implement task with this feedback' },
430
+ { key: 'ignore', label: 'Ignore failures and approve anyway' }
431
+ ],
432
+ allowCustom: true
433
+ });
434
+
435
+ const failRaw = await askHuman(formatPrompt(failChoice), {
436
+ slug: failChoice.slug,
437
+ interaction: failChoice
438
+ });
439
+ const failResponse = await parseResponse(failChoice, failRaw);
440
+
441
+ if (failResponse.selectedKey === 'reimplement' || failResponse.isCustom) {
442
+ setTaskData(i, taskId, 'feedback', `Sanity check failures:\n${failedChecks}`);
443
+ setTaskData(i, taskId, 'security_pre', null);
444
+ setTaskData(i, taskId, 'tests', null);
445
+ setTaskData(i, taskId, 'code', null);
446
+ setTaskData(i, taskId, 'review', null);
447
+ setTaskData(i, taskId, 'security_post', null);
448
+ setTaskStage(i, taskId, TASK_STAGES.PENDING);
449
+ t--;
450
+ continue;
451
+ }
452
+ }
453
+
454
+ setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
455
+ stage = TASK_STAGES.COMPLETED;
456
+ task.stage = 'completed';
457
+ memory[tasksKey] = tasks;
458
+ writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
459
+ console.log(` Task ${t + 1} confirmed complete!\n`);
460
+ } else if (action === 'skip') {
461
+ setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
462
+ stage = TASK_STAGES.COMPLETED;
463
+ task.stage = 'completed';
464
+ memory[tasksKey] = tasks;
465
+ writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
466
+ console.log(` Task ${t + 1} confirmed complete!\n`);
467
+ } else {
468
+ setTaskStage(i, taskId, TASK_STAGES.AWAITING_APPROVAL);
469
+ stage = TASK_STAGES.AWAITING_APPROVAL;
470
+ }
471
+ }
472
+
473
+ // 7. Manual approval (for when user runs checks)
334
474
  if (stage === TASK_STAGES.AWAITING_APPROVAL) {
335
- const sanityCheck = await askHuman(
336
- `Task ${t + 1} (${task.title}) complete.\n\nDefinition of Done: ${task.doneDefinition || 'Task completed successfully'}\n\nSanity Check: ${task.sanityCheck || 'Review the implementation and confirm it meets requirements.'}\n\nOptions:\n- A: Confirm task completion\n- B: Flag issue (describe the problem)\n\nYour response:`,
337
- { slug: `phase-${i + 1}-task-${taskId}-sanity` }
338
- );
475
+ const approvalInteraction = createInteraction('choice', `phase-${i + 1}-task-${taskId}-approval`, {
476
+ prompt: `Task ${t + 1} (${task.title}) complete.\n\nDefinition of Done: ${task.doneDefinition || 'Task completed successfully'}`,
477
+ options: [
478
+ { key: 'approve', label: 'Confirm task completion' },
479
+ { key: 'issue', label: 'Flag issue', description: 'Describe the problem' }
480
+ ],
481
+ allowCustom: true
482
+ });
339
483
 
340
- if (isApproval(sanityCheck)) {
341
- // Mark task complete
484
+ const approvalRaw = await askHuman(formatPrompt(approvalInteraction), {
485
+ slug: approvalInteraction.slug,
486
+ interaction: approvalInteraction
487
+ });
488
+ const approvalResponse = await parseResponse(approvalInteraction, approvalRaw);
489
+
490
+ if (approvalResponse.selectedKey === 'approve' || isApproval(approvalResponse.raw || approvalRaw)) {
342
491
  setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
343
492
  task.stage = 'completed';
344
- memory[tasksKey] = tasks; // Persist updated tasks
493
+ memory[tasksKey] = tasks;
345
494
  writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
346
495
  console.log(` Task ${t + 1} confirmed complete!\n`);
347
496
  } else {
348
- // Store feedback and reset task for reprocessing
349
497
  console.log(' > Issue flagged, reprocessing task with feedback...');
350
- setTaskData(i, taskId, 'feedback', sanityCheck);
498
+ const feedbackText = approvalResponse.customText || approvalResponse.text || approvalResponse.raw || approvalRaw;
499
+ setTaskData(i, taskId, 'feedback', feedbackText);
351
500
 
352
- // Clear previous outputs to force regeneration
353
501
  setTaskData(i, taskId, 'security_pre', null);
354
502
  setTaskData(i, taskId, 'tests', null);
355
503
  setTaskData(i, taskId, 'code', null);
356
504
  setTaskData(i, taskId, 'review', null);
357
505
  setTaskData(i, taskId, 'security_post', null);
506
+ setTaskData(i, taskId, 'sanity_checks', null);
507
+ setTaskData(i, taskId, 'sanity_results', null);
358
508
 
359
- // Reset to pending and reprocess same task
360
509
  setTaskStage(i, taskId, TASK_STAGES.PENDING);
361
- t--; // Reprocess this task
510
+ t--;
362
511
  }
363
512
  }
364
513
 
@@ -366,16 +515,27 @@ export default async function () {
366
515
  console.error(` Task ${t + 1} failed: ${error.message}`);
367
516
  setTaskStage(i, taskId, TASK_STAGES.FAILED);
368
517
 
369
- const retry = await askHuman(
370
- `Task "${task.title}" failed with error: ${error.message}\n\nOptions:\n- A: Retry this task\n- B: Skip and continue\n- C: Abort workflow\n\nYour choice:`,
371
- { slug: `phase-${i + 1}-task-${taskId}-error` }
372
- );
518
+ const retryInteraction = createInteraction('choice', `phase-${i + 1}-task-${taskId}-error`, {
519
+ prompt: `Task "${task.title}" failed with error: ${error.message}\n\nHow would you like to proceed?`,
520
+ options: [
521
+ { key: 'retry', label: 'Retry this task' },
522
+ { key: 'skip', label: 'Skip and continue' },
523
+ { key: 'abort', label: 'Abort workflow' }
524
+ ],
525
+ allowCustom: true
526
+ });
527
+
528
+ const retryRaw = await askHuman(formatPrompt(retryInteraction), {
529
+ slug: retryInteraction.slug,
530
+ interaction: retryInteraction
531
+ });
532
+ const retryResponse = await parseResponse(retryInteraction, retryRaw);
533
+ const retryValue = (retryResponse.raw || retryRaw).trim().toLowerCase();
373
534
 
374
- const retryTrimmed = retry.trim().toLowerCase();
375
- if (retryTrimmed.startsWith('a') || retryTrimmed.startsWith('retry')) {
535
+ if (retryResponse.selectedKey === 'retry' || retryValue.startsWith('a') || retryValue.startsWith('retry')) {
376
536
  setTaskStage(i, taskId, TASK_STAGES.PENDING);
377
537
  t--; // Retry this task
378
- } else if (retryTrimmed.startsWith('c') || retryTrimmed.startsWith('abort')) {
538
+ } else if (retryResponse.selectedKey === 'abort' || retryValue.startsWith('c') || retryValue.startsWith('abort')) {
379
539
  throw new Error('Workflow aborted by user');
380
540
  }
381
541
  // Otherwise skip and continue to next task
@@ -11,8 +11,8 @@ const __dirname = path.dirname(__filename);
11
11
  let cachedTemplate = null;
12
12
  async function getTemplate() {
13
13
  if (cachedTemplate) return cachedTemplate;
14
- // Point to the unified template in vercel-server/ui/index.html
15
- const templatePath = path.join(__dirname, '..', '..', 'ui', 'index.html');
14
+ // Point to the built template in vercel-server/public/remote/index.html
15
+ const templatePath = path.join(__dirname, '..', '..', 'public', 'remote', 'index.html');
16
16
  cachedTemplate = await readFile(templatePath, 'utf8');
17
17
  return cachedTemplate;
18
18
  }
@@ -46,4 +46,4 @@ export default async function handler(req, res) {
46
46
 
47
47
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
48
48
  res.send(html);
49
- }
49
+ }
@@ -47,12 +47,14 @@ export default async function handler(req, res) {
47
47
  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
48
48
  const { slug, targetKey, response } = body;
49
49
 
50
- if (!slug || !response) {
50
+ if (!slug || response === undefined || response === null) {
51
51
  return res.status(400).json({ error: 'Missing required fields: slug, response' });
52
52
  }
53
53
 
54
+ const responseString = typeof response === 'string' ? response : JSON.stringify(response);
55
+
54
56
  // Validate response size (max 1MB)
55
- if (response.length > 1024 * 1024) {
57
+ if (responseString.length > 1024 * 1024) {
56
58
  return res.status(413).json({ error: 'Response too large (max 1MB)' });
57
59
  }
58
60
 
@@ -73,7 +75,7 @@ export default async function handler(req, res) {
73
75
  event: 'INTERACTION_SUBMITTED',
74
76
  slug,
75
77
  targetKey: targetKey || `_interaction_${slug}`,
76
- answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
78
+ answer: responseString.substring(0, 200) + (responseString.length > 200 ? '...' : ''),
77
79
  source: 'remote',
78
80
  });
79
81
 
@@ -24,6 +24,7 @@ const PORT = process.env.PORT || 3001;
24
24
 
25
25
  // In-memory session storage
26
26
  const sessions = new Map();
27
+ let latestSessionToken = null;
27
28
 
28
29
  // SSE clients per session
29
30
  const sseClients = new Map(); // token -> Set<res>
@@ -44,6 +45,7 @@ function createSession(token, data) {
44
45
  createdAt: Date.now(),
45
46
  };
46
47
  sessions.set(token, session);
48
+ latestSessionToken = token;
47
49
  return session;
48
50
  }
49
51
 
@@ -308,10 +310,12 @@ async function handleSubmitPost(req, res, token) {
308
310
  const body = await parseBody(req);
309
311
  const { slug, targetKey, response } = body;
310
312
 
311
- if (!slug || !response) {
313
+ if (!slug || response === undefined || response === null) {
312
314
  return sendJson(res, 400, { error: 'Missing slug or response' });
313
315
  }
314
316
 
317
+ const responseString = typeof response === 'string' ? response : JSON.stringify(response);
318
+
315
319
  // Add to pending interactions for CLI to pick up
316
320
  session.pendingInteractions.push({
317
321
  slug,
@@ -325,7 +329,7 @@ async function handleSubmitPost(req, res, token) {
325
329
  event: 'INTERACTION_SUBMITTED',
326
330
  slug,
327
331
  targetKey: targetKey || `_interaction_${slug}`,
328
- answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
332
+ answer: responseString.substring(0, 200) + (responseString.length > 200 ? '...' : ''),
329
333
  source: 'remote',
330
334
  };
331
335
  session.history.unshift(event);
@@ -342,10 +346,10 @@ async function handleSubmitPost(req, res, token) {
342
346
  /**
343
347
  * Serve session UI
344
348
  */
345
- const MASTER_TEMPLATE_PATH = path.join(__dirname, 'ui', 'index.html');
349
+ const MASTER_TEMPLATE_PATH = path.join(__dirname, 'public', 'remote', 'index.html');
346
350
 
347
351
  /**
348
- * Get session HTML by reading the master template from lib/ui/index.html
352
+ * Get session HTML by reading the master template from public/remote/index.html
349
353
  */
350
354
  function getSessionHTML(token, workflowName) {
351
355
  try {
@@ -362,7 +366,8 @@ function getSessionHTML(token, workflowName) {
362
366
  <body style="font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center;">
363
367
  <h1>Error loading UI template</h1>
364
368
  <p>${err.message}</p>
365
- <p>Make sure <code>lib/ui/index.html</code> exists.</p>
369
+ <p>Make sure <code>public/remote/index.html</code> exists.</p>
370
+ <p>Build the UI first: <code>cd vercel-server/ui && npm install && npm run build</code></p>
366
371
  </body>
367
372
  </html>
368
373
  `;
@@ -395,6 +400,16 @@ function serveSessionUI(res, token) {
395
400
 
396
401
  // getSessionHTML was moved up and updated to read from MASTER_TEMPLATE_PATH
397
402
 
403
+ function getDefaultSessionToken() {
404
+ if (latestSessionToken && sessions.has(latestSessionToken)) {
405
+ return latestSessionToken;
406
+ }
407
+ if (sessions.size === 1) {
408
+ return sessions.keys().next().value;
409
+ }
410
+ return null;
411
+ }
412
+
398
413
  /**
399
414
  * Serve static files
400
415
  */
@@ -470,10 +485,22 @@ async function handleRequest(req, res) {
470
485
  }
471
486
 
472
487
  // Route: Static files
473
- if (pathname === '/' || pathname === '/index.html') {
488
+ if (pathname === '/') {
489
+ const defaultToken = getDefaultSessionToken();
490
+ if (defaultToken) {
491
+ return serveSessionUI(res, defaultToken);
492
+ }
474
493
  return serveStatic(res, 'index.html');
475
494
  }
476
495
 
496
+ if (pathname === '/index.html') {
497
+ return serveStatic(res, 'index.html');
498
+ }
499
+
500
+ if (pathname.startsWith('/remote/')) {
501
+ return serveStatic(res, pathname.slice(1));
502
+ }
503
+
477
504
  // 404
478
505
  res.writeHead(404);
479
506
  res.end('Not found');
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <title>{{WORKFLOW_NAME}}</title>
7
+ <script type="module" crossorigin src="/remote/assets/index-DiKoT5IY.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/remote/assets/index-dzq5qHNh.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script>
13
+ window.SESSION_TOKEN = "{{SESSION_TOKEN}}";
14
+ window.WORKFLOW_NAME_TEMPLATE = "{{WORKFLOW_NAME}}";
15
+ </script>
16
+ </body>
17
+ </html>