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.
- package/bin/cli.js +1 -1
- package/lib/index.js +33 -0
- package/lib/remote/client.js +7 -2
- package/lib/runtime/agent.js +102 -67
- package/lib/runtime/index.js +13 -0
- package/lib/runtime/interaction.js +304 -0
- package/lib/runtime/prompt.js +39 -12
- package/lib/runtime/runtime.js +11 -10
- package/package.json +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
- package/templates/project-builder/agents/code-reviewer.md +0 -1
- package/templates/project-builder/agents/code-writer.md +0 -1
- package/templates/project-builder/agents/requirements-clarifier.md +0 -1
- package/templates/project-builder/agents/response-interpreter.md +25 -0
- package/templates/project-builder/agents/roadmap-generator.md +0 -1
- package/templates/project-builder/agents/sanity-checker.md +45 -0
- package/templates/project-builder/agents/sanity-runner.js +161 -0
- package/templates/project-builder/agents/scope-clarifier.md +0 -1
- package/templates/project-builder/agents/security-clarifier.md +0 -1
- package/templates/project-builder/agents/security-reviewer.md +0 -1
- package/templates/project-builder/agents/task-planner.md +0 -1
- package/templates/project-builder/agents/test-planner.md +0 -1
- package/templates/project-builder/scripts/interaction-helpers.js +33 -0
- package/templates/project-builder/scripts/workflow-helpers.js +2 -47
- package/templates/project-builder/workflow.js +214 -54
- package/vercel-server/api/session/[token].js +3 -3
- package/vercel-server/api/submit/[token].js +5 -3
- package/vercel-server/local-server.js +33 -6
- package/vercel-server/public/remote/index.html +17 -0
- package/vercel-server/ui/index.html +9 -1012
- package/vercel-server/ui/package-lock.json +2650 -0
- package/vercel-server/ui/package.json +25 -0
- package/vercel-server/ui/postcss.config.js +6 -0
- package/vercel-server/ui/src/App.jsx +236 -0
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
- package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
- package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
- package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
- package/vercel-server/ui/src/components/Footer.jsx +66 -0
- package/vercel-server/ui/src/components/Header.jsx +38 -0
- package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
- package/vercel-server/ui/src/index.css +145 -0
- package/vercel-server/ui/src/main.jsx +8 -0
- package/vercel-server/ui/tailwind.config.js +19 -0
- 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
|
|
47
|
-
'Describe the project you want to build. Include any initial requirements, goals, or constraints you have in mind.',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
197
|
-
`Please review the task list for Phase ${i + 1} in state/phase-${i + 1}-tasks.md
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
330
|
-
stage = TASK_STAGES.
|
|
370
|
+
setTaskStage(i, taskId, TASK_STAGES.SANITY_CHECK);
|
|
371
|
+
stage = TASK_STAGES.SANITY_CHECK;
|
|
331
372
|
}
|
|
332
373
|
|
|
333
|
-
// 6. Sanity check
|
|
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
|
|
336
|
-
`Task ${t + 1} (${task.title}) complete.\n\nDefinition of Done: ${task.doneDefinition || 'Task completed successfully'}
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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;
|
|
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
|
-
|
|
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--;
|
|
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
|
|
370
|
-
`Task "${task.title}" failed with error: ${error.message}\n\
|
|
371
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
15
|
-
const templatePath = path.join(__dirname, '..', '..', '
|
|
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 ||
|
|
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 (
|
|
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:
|
|
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 ||
|
|
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:
|
|
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, '
|
|
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
|
|
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>
|
|
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 === '/'
|
|
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>
|