agent-state-machine 2.0.14 → 2.1.0
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 +2 -1
- package/templates/project-builder/README.md +119 -0
- package/templates/project-builder/agents/assumptions-clarifier.md +65 -0
- package/templates/project-builder/agents/code-reviewer.md +81 -0
- package/templates/project-builder/agents/code-writer.md +74 -0
- package/templates/project-builder/agents/requirements-clarifier.md +55 -0
- package/templates/project-builder/agents/response-interpreter.md +25 -0
- package/templates/project-builder/agents/roadmap-generator.md +73 -0
- 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 +44 -0
- package/templates/project-builder/agents/security-clarifier.md +71 -0
- package/templates/project-builder/agents/security-reviewer.md +71 -0
- package/templates/project-builder/agents/task-planner.md +62 -0
- package/templates/project-builder/agents/test-planner.md +76 -0
- package/templates/project-builder/config.js +13 -0
- package/templates/project-builder/scripts/interaction-helpers.js +33 -0
- package/templates/project-builder/scripts/mac-notification.js +24 -0
- package/templates/project-builder/scripts/text-human.js +92 -0
- package/templates/project-builder/scripts/workflow-helpers.js +122 -0
- package/templates/project-builder/state/current.json +9 -0
- package/templates/project-builder/state/history.jsonl +0 -0
- package/templates/project-builder/steering/config.json +5 -0
- package/templates/project-builder/steering/global.md +19 -0
- package/templates/project-builder/workflow.js +554 -0
- package/templates/starter/README.md +118 -0
- package/templates/starter/agents/example.js +36 -0
- package/templates/starter/agents/yoda-greeter.md +12 -0
- package/templates/starter/agents/yoda-name-collector.md +12 -0
- package/templates/starter/config.js +12 -0
- package/templates/starter/interactions/.gitkeep +0 -0
- package/templates/starter/scripts/mac-notification.js +24 -0
- package/templates/starter/state/current.json +9 -0
- package/templates/starter/state/history.jsonl +0 -0
- package/templates/starter/steering/config.json +5 -0
- package/templates/starter/steering/global.md +19 -0
- package/templates/starter/workflow.js +52 -0
- 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
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Builder Workflow
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive workflow that guides users through:
|
|
5
|
+
* 1. Project intake and clarification
|
|
6
|
+
* 2. Phased roadmap generation and approval
|
|
7
|
+
* 3. Sequential phase execution with task lists
|
|
8
|
+
* 4. Task lifecycle with optimal agent sequencing
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { agent, memory, askHuman } from 'agent-state-machine';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import {
|
|
15
|
+
writeMarkdownFile,
|
|
16
|
+
isApproval,
|
|
17
|
+
renderRoadmapMarkdown,
|
|
18
|
+
renderTasksMarkdown,
|
|
19
|
+
TASK_STAGES,
|
|
20
|
+
getTaskStage,
|
|
21
|
+
setTaskStage,
|
|
22
|
+
getTaskData,
|
|
23
|
+
setTaskData
|
|
24
|
+
} from './scripts/workflow-helpers.js';
|
|
25
|
+
import {
|
|
26
|
+
createInteraction,
|
|
27
|
+
parseResponse,
|
|
28
|
+
formatPrompt
|
|
29
|
+
} from './scripts/interaction-helpers.js';
|
|
30
|
+
|
|
31
|
+
// Derive workflow directory dynamically
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = path.dirname(__filename);
|
|
34
|
+
const WORKFLOW_DIR = __dirname;
|
|
35
|
+
const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// MAIN WORKFLOW
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
export default async function () {
|
|
42
|
+
console.log('Starting Project Builder Workflow...\n');
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// PHASE 1: PROJECT INTAKE
|
|
46
|
+
// ============================================
|
|
47
|
+
console.log('=== PHASE 1: PROJECT INTAKE ===\n');
|
|
48
|
+
|
|
49
|
+
if (!memory.projectDescription) {
|
|
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;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('Project description captured. Starting clarification process...\n');
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// CLARIFICATION AGENTS (Optimal Sequence)
|
|
67
|
+
// Order: Scope -> Requirements -> Assumptions -> Security
|
|
68
|
+
// Runtime handles interaction blocking automatically
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
// 1. Scope Clarification
|
|
72
|
+
if (!memory.scopeClarified) {
|
|
73
|
+
console.log('--- Scope Clarification ---');
|
|
74
|
+
const scopeResult = await agent('scope-clarifier', {
|
|
75
|
+
projectDescription: memory.projectDescription
|
|
76
|
+
});
|
|
77
|
+
memory.scope = scopeResult;
|
|
78
|
+
memory.scopeClarified = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Requirements Clarification
|
|
82
|
+
if (!memory.requirementsClarified) {
|
|
83
|
+
console.log('--- Requirements Clarification ---');
|
|
84
|
+
const reqResult = await agent('requirements-clarifier', {
|
|
85
|
+
projectDescription: memory.projectDescription,
|
|
86
|
+
scope: memory.scope
|
|
87
|
+
});
|
|
88
|
+
memory.requirements = reqResult;
|
|
89
|
+
memory.requirementsClarified = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Assumptions Clarification
|
|
93
|
+
if (!memory.assumptionsClarified) {
|
|
94
|
+
console.log('--- Assumptions Clarification ---');
|
|
95
|
+
const assumeResult = await agent('assumptions-clarifier', {
|
|
96
|
+
projectDescription: memory.projectDescription,
|
|
97
|
+
scope: memory.scope,
|
|
98
|
+
requirements: memory.requirements
|
|
99
|
+
});
|
|
100
|
+
memory.assumptions = assumeResult;
|
|
101
|
+
memory.assumptionsClarified = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Security Clarification
|
|
105
|
+
if (!memory.securityClarified) {
|
|
106
|
+
console.log('--- Security Clarification ---');
|
|
107
|
+
const secResult = await agent('security-clarifier', {
|
|
108
|
+
projectDescription: memory.projectDescription,
|
|
109
|
+
scope: memory.scope,
|
|
110
|
+
requirements: memory.requirements,
|
|
111
|
+
assumptions: memory.assumptions
|
|
112
|
+
});
|
|
113
|
+
memory.security = secResult;
|
|
114
|
+
memory.securityClarified = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log('\nClarification complete. Generating roadmap...\n');
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// PHASE 2: PHASED ROADMAP
|
|
121
|
+
// ============================================
|
|
122
|
+
console.log('=== PHASE 2: PHASED ROADMAP ===\n');
|
|
123
|
+
|
|
124
|
+
if (!memory.roadmapApproved) {
|
|
125
|
+
// Generate roadmap as JSON
|
|
126
|
+
if (!memory.roadmap) {
|
|
127
|
+
const roadmapResult = await agent('roadmap-generator', {
|
|
128
|
+
projectDescription: memory.projectDescription,
|
|
129
|
+
scope: memory.scope,
|
|
130
|
+
requirements: memory.requirements,
|
|
131
|
+
assumptions: memory.assumptions,
|
|
132
|
+
security: memory.security
|
|
133
|
+
});
|
|
134
|
+
memory.roadmap = roadmapResult;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeMarkdownFile(STATE_DIR, 'roadmap.md', renderRoadmapMarkdown(memory.roadmap));
|
|
138
|
+
|
|
139
|
+
// Roadmap approval loop
|
|
140
|
+
let approved = false;
|
|
141
|
+
while (!approved) {
|
|
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);
|
|
156
|
+
|
|
157
|
+
if (reviewResponse.selectedKey === 'approve' || isApproval(reviewResponse.raw || reviewRaw)) {
|
|
158
|
+
approved = true;
|
|
159
|
+
memory.roadmapApproved = true;
|
|
160
|
+
console.log('Roadmap approved!\n');
|
|
161
|
+
} else {
|
|
162
|
+
const feedback = reviewResponse.customText || reviewResponse.text || reviewResponse.raw || reviewRaw;
|
|
163
|
+
// Regenerate roadmap with feedback
|
|
164
|
+
const updatedRoadmap = await agent('roadmap-generator', {
|
|
165
|
+
projectDescription: memory.projectDescription,
|
|
166
|
+
scope: memory.scope,
|
|
167
|
+
requirements: memory.requirements,
|
|
168
|
+
assumptions: memory.assumptions,
|
|
169
|
+
security: memory.security,
|
|
170
|
+
feedback
|
|
171
|
+
});
|
|
172
|
+
memory.roadmap = updatedRoadmap;
|
|
173
|
+
writeMarkdownFile(STATE_DIR, 'roadmap.md', renderRoadmapMarkdown(memory.roadmap));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================
|
|
179
|
+
// PHASE 3: PHASE EXECUTION
|
|
180
|
+
// ============================================
|
|
181
|
+
console.log('=== PHASE 3: PHASE EXECUTION ===\n');
|
|
182
|
+
|
|
183
|
+
// Initialize phase tracking with proper undefined check
|
|
184
|
+
if (memory.currentPhaseIndex === undefined) {
|
|
185
|
+
memory.currentPhaseIndex = 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const phases = memory.roadmap?.phases || [];
|
|
189
|
+
|
|
190
|
+
// Sequential phase processing
|
|
191
|
+
for (let i = memory.currentPhaseIndex; i < phases.length; i++) {
|
|
192
|
+
memory.currentPhaseIndex = i;
|
|
193
|
+
const phase = phases[i];
|
|
194
|
+
console.log(`\n--- Processing Phase ${i + 1}: ${phase.title} ---\n`);
|
|
195
|
+
|
|
196
|
+
const tasksKey = `phase_${i}_tasks`;
|
|
197
|
+
const tasksApprovedKey = `phase_${i}_tasks_approved`;
|
|
198
|
+
|
|
199
|
+
// Generate task list for this phase (as JSON)
|
|
200
|
+
if (!memory[tasksApprovedKey]) {
|
|
201
|
+
if (!memory[tasksKey]) {
|
|
202
|
+
const taskResult = await agent('task-planner', {
|
|
203
|
+
projectDescription: memory.projectDescription,
|
|
204
|
+
scope: memory.scope,
|
|
205
|
+
requirements: memory.requirements,
|
|
206
|
+
phase: phase,
|
|
207
|
+
phaseIndex: i + 1
|
|
208
|
+
});
|
|
209
|
+
memory[tasksKey] = taskResult;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, memory[tasksKey]?.tasks || memory[tasksKey]));
|
|
213
|
+
|
|
214
|
+
// Task list approval loop
|
|
215
|
+
let tasksApproved = false;
|
|
216
|
+
while (!tasksApproved) {
|
|
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
|
+
});
|
|
225
|
+
|
|
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)) {
|
|
233
|
+
tasksApproved = true;
|
|
234
|
+
memory[tasksApprovedKey] = true;
|
|
235
|
+
console.log(`Phase ${i + 1} task list approved!\n`);
|
|
236
|
+
} else {
|
|
237
|
+
const feedback = taskReview.customText || taskReview.text || taskReview.raw || taskReviewRaw;
|
|
238
|
+
const updatedTasks = await agent('task-planner', {
|
|
239
|
+
projectDescription: memory.projectDescription,
|
|
240
|
+
scope: memory.scope,
|
|
241
|
+
requirements: memory.requirements,
|
|
242
|
+
phase: phase,
|
|
243
|
+
phaseIndex: i + 1,
|
|
244
|
+
feedback
|
|
245
|
+
});
|
|
246
|
+
memory[tasksKey] = updatedTasks;
|
|
247
|
+
writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, memory[tasksKey]?.tasks || memory[tasksKey]));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================
|
|
253
|
+
// TASK LIFECYCLE WITH IDEMPOTENCY
|
|
254
|
+
// ============================================
|
|
255
|
+
console.log(`\n=== TASK LIFECYCLE: Phase ${i + 1} ===\n`);
|
|
256
|
+
|
|
257
|
+
const tasks = memory[tasksKey]?.tasks || memory[tasksKey] || [];
|
|
258
|
+
const taskIndexKey = `phase_${i}_task_index`;
|
|
259
|
+
|
|
260
|
+
// Fix: use undefined check instead of falsy check
|
|
261
|
+
if (memory[taskIndexKey] === undefined) {
|
|
262
|
+
memory[taskIndexKey] = 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Process each task with optimal agent ordering and idempotency
|
|
266
|
+
for (let t = memory[taskIndexKey]; t < tasks.length; t++) {
|
|
267
|
+
memory[taskIndexKey] = t;
|
|
268
|
+
const task = tasks[t];
|
|
269
|
+
const taskId = task.id || t;
|
|
270
|
+
|
|
271
|
+
console.log(`\n Task ${t + 1}/${tasks.length}: ${task.title}\n`);
|
|
272
|
+
|
|
273
|
+
// Get current stage for this task
|
|
274
|
+
let stage = getTaskStage(i, taskId);
|
|
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
|
+
|
|
285
|
+
// Store any feedback for this task
|
|
286
|
+
const feedback = getTaskData(i, taskId, 'feedback');
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// 1. Security Review (pre-implementation)
|
|
290
|
+
if (stage === TASK_STAGES.PENDING || stage === TASK_STAGES.SECURITY_PRE) {
|
|
291
|
+
if (!getTaskData(i, taskId, 'security_pre')) {
|
|
292
|
+
console.log(' > Security pre-review...');
|
|
293
|
+
const securityPreReview = await agent('security-reviewer', {
|
|
294
|
+
task: task,
|
|
295
|
+
phase: phase,
|
|
296
|
+
scope: memory.scope,
|
|
297
|
+
stage: 'pre-implementation',
|
|
298
|
+
feedback: feedback
|
|
299
|
+
});
|
|
300
|
+
setTaskData(i, taskId, 'security_pre', securityPreReview);
|
|
301
|
+
}
|
|
302
|
+
setTaskStage(i, taskId, TASK_STAGES.TEST_PLANNING);
|
|
303
|
+
stage = TASK_STAGES.TEST_PLANNING;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 2. Test Planning
|
|
307
|
+
if (stage === TASK_STAGES.TEST_PLANNING) {
|
|
308
|
+
if (!getTaskData(i, taskId, 'tests')) {
|
|
309
|
+
console.log(' > Test planning...');
|
|
310
|
+
const testPlan = await agent('test-planner', {
|
|
311
|
+
task: task,
|
|
312
|
+
phase: phase,
|
|
313
|
+
requirements: memory.requirements,
|
|
314
|
+
securityConsiderations: getTaskData(i, taskId, 'security_pre'),
|
|
315
|
+
feedback: feedback
|
|
316
|
+
});
|
|
317
|
+
setTaskData(i, taskId, 'tests', testPlan);
|
|
318
|
+
}
|
|
319
|
+
setTaskStage(i, taskId, TASK_STAGES.IMPLEMENTING);
|
|
320
|
+
stage = TASK_STAGES.IMPLEMENTING;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 3. Code Writing
|
|
324
|
+
if (stage === TASK_STAGES.IMPLEMENTING) {
|
|
325
|
+
if (!getTaskData(i, taskId, 'code')) {
|
|
326
|
+
console.log(' > Code implementation...');
|
|
327
|
+
const implementation = await agent('code-writer', {
|
|
328
|
+
task: task,
|
|
329
|
+
phase: phase,
|
|
330
|
+
requirements: memory.requirements,
|
|
331
|
+
testPlan: getTaskData(i, taskId, 'tests'),
|
|
332
|
+
securityConsiderations: getTaskData(i, taskId, 'security_pre'),
|
|
333
|
+
feedback: feedback
|
|
334
|
+
});
|
|
335
|
+
setTaskData(i, taskId, 'code', implementation);
|
|
336
|
+
}
|
|
337
|
+
setTaskStage(i, taskId, TASK_STAGES.CODE_REVIEW);
|
|
338
|
+
stage = TASK_STAGES.CODE_REVIEW;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 4. Code Review
|
|
342
|
+
if (stage === TASK_STAGES.CODE_REVIEW) {
|
|
343
|
+
if (!getTaskData(i, taskId, 'review')) {
|
|
344
|
+
console.log(' > Code review...');
|
|
345
|
+
const codeReview = await agent('code-reviewer', {
|
|
346
|
+
task: task,
|
|
347
|
+
implementation: getTaskData(i, taskId, 'code'),
|
|
348
|
+
testPlan: getTaskData(i, taskId, 'tests'),
|
|
349
|
+
feedback: feedback
|
|
350
|
+
});
|
|
351
|
+
setTaskData(i, taskId, 'review', codeReview);
|
|
352
|
+
}
|
|
353
|
+
setTaskStage(i, taskId, TASK_STAGES.SECURITY_POST);
|
|
354
|
+
stage = TASK_STAGES.SECURITY_POST;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 5. Final Security Check
|
|
358
|
+
if (stage === TASK_STAGES.SECURITY_POST) {
|
|
359
|
+
if (!getTaskData(i, taskId, 'security_post')) {
|
|
360
|
+
console.log(' > Final security check...');
|
|
361
|
+
const securityPostReview = await agent('security-reviewer', {
|
|
362
|
+
task: task,
|
|
363
|
+
phase: phase,
|
|
364
|
+
implementation: getTaskData(i, taskId, 'code'),
|
|
365
|
+
stage: 'post-implementation',
|
|
366
|
+
feedback: feedback
|
|
367
|
+
});
|
|
368
|
+
setTaskData(i, taskId, 'security_post', securityPostReview);
|
|
369
|
+
}
|
|
370
|
+
setTaskStage(i, taskId, TASK_STAGES.SANITY_CHECK);
|
|
371
|
+
stage = TASK_STAGES.SANITY_CHECK;
|
|
372
|
+
}
|
|
373
|
+
|
|
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)
|
|
474
|
+
if (stage === TASK_STAGES.AWAITING_APPROVAL) {
|
|
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
|
+
});
|
|
483
|
+
|
|
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)) {
|
|
491
|
+
setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
|
|
492
|
+
task.stage = 'completed';
|
|
493
|
+
memory[tasksKey] = tasks;
|
|
494
|
+
writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
|
|
495
|
+
console.log(` Task ${t + 1} confirmed complete!\n`);
|
|
496
|
+
} else {
|
|
497
|
+
console.log(' > Issue flagged, reprocessing task with feedback...');
|
|
498
|
+
const feedbackText = approvalResponse.customText || approvalResponse.text || approvalResponse.raw || approvalRaw;
|
|
499
|
+
setTaskData(i, taskId, 'feedback', feedbackText);
|
|
500
|
+
|
|
501
|
+
setTaskData(i, taskId, 'security_pre', null);
|
|
502
|
+
setTaskData(i, taskId, 'tests', null);
|
|
503
|
+
setTaskData(i, taskId, 'code', null);
|
|
504
|
+
setTaskData(i, taskId, 'review', null);
|
|
505
|
+
setTaskData(i, taskId, 'security_post', null);
|
|
506
|
+
setTaskData(i, taskId, 'sanity_checks', null);
|
|
507
|
+
setTaskData(i, taskId, 'sanity_results', null);
|
|
508
|
+
|
|
509
|
+
setTaskStage(i, taskId, TASK_STAGES.PENDING);
|
|
510
|
+
t--;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error(` Task ${t + 1} failed: ${error.message}`);
|
|
516
|
+
setTaskStage(i, taskId, TASK_STAGES.FAILED);
|
|
517
|
+
|
|
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();
|
|
534
|
+
|
|
535
|
+
if (retryResponse.selectedKey === 'retry' || retryValue.startsWith('a') || retryValue.startsWith('retry')) {
|
|
536
|
+
setTaskStage(i, taskId, TASK_STAGES.PENDING);
|
|
537
|
+
t--; // Retry this task
|
|
538
|
+
} else if (retryResponse.selectedKey === 'abort' || retryValue.startsWith('c') || retryValue.startsWith('abort')) {
|
|
539
|
+
throw new Error('Workflow aborted by user');
|
|
540
|
+
}
|
|
541
|
+
// Otherwise skip and continue to next task
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Mark phase complete in roadmap
|
|
546
|
+
phase.completed = true;
|
|
547
|
+
memory.roadmap.phases[i] = phase;
|
|
548
|
+
writeMarkdownFile(STATE_DIR, 'roadmap.md', renderRoadmapMarkdown(memory.roadmap));
|
|
549
|
+
console.log(`\nPhase ${i + 1} completed!\n`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log('\n=== PROJECT BUILD COMPLETE ===\n');
|
|
553
|
+
memory.projectComplete = true;
|
|
554
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# __WORKFLOW_NAME__
|
|
2
|
+
|
|
3
|
+
A workflow created with agent-state-machine (native JS format).
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
__WORKFLOW_NAME__/
|
|
9
|
+
├── workflow.js # Native JS workflow (async/await)
|
|
10
|
+
├── config.js # Model/API key configuration
|
|
11
|
+
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
12
|
+
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
13
|
+
├── state/ # Runtime state (current.json, history.jsonl)
|
|
14
|
+
└── steering/ # Steering configuration
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Edit `config.js` to set models and API keys for this workflow.
|
|
20
|
+
|
|
21
|
+
Run the workflow (or resume if interrupted):
|
|
22
|
+
```bash
|
|
23
|
+
state-machine run __WORKFLOW_NAME__
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Check status:
|
|
27
|
+
```bash
|
|
28
|
+
state-machine status __WORKFLOW_NAME__
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
View history:
|
|
32
|
+
```bash
|
|
33
|
+
state-machine history __WORKFLOW_NAME__
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
View trace logs in browser with live updates:
|
|
37
|
+
```bash
|
|
38
|
+
state-machine follow __WORKFLOW_NAME__
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Reset state (clears memory/state):
|
|
42
|
+
```bash
|
|
43
|
+
state-machine reset __WORKFLOW_NAME__
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Hard reset (clears everything: history/interactions/memory):
|
|
47
|
+
```bash
|
|
48
|
+
state-machine reset-hard __WORKFLOW_NAME__
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Writing Workflows
|
|
52
|
+
|
|
53
|
+
Edit `workflow.js` - write normal async JavaScript:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
57
|
+
|
|
58
|
+
export default async function() {
|
|
59
|
+
console.log('Starting __WORKFLOW_NAME__ workflow...');
|
|
60
|
+
|
|
61
|
+
// Example: Get user input (saved to memory)
|
|
62
|
+
const userLocation = await askHuman('Where do you live?');
|
|
63
|
+
console.log('Example prompt answer:', userLocation);
|
|
64
|
+
|
|
65
|
+
const userInfo = await agent('yoda-name-collector');
|
|
66
|
+
memory.userInfo = userInfo;
|
|
67
|
+
|
|
68
|
+
// Provide context
|
|
69
|
+
// const userInfo = await agent('yoda-name-collector', { name: 'Luke' });
|
|
70
|
+
|
|
71
|
+
console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
|
|
72
|
+
|
|
73
|
+
// Context is explicit: pass what the agent needs
|
|
74
|
+
const { greeting } = await agent('yoda-greeter', { userLocation, memory });
|
|
75
|
+
console.log('Example agent greeting:', greeting);
|
|
76
|
+
|
|
77
|
+
// Or you can provide context manually
|
|
78
|
+
// await agent('yoda-greeter', userInfo);
|
|
79
|
+
|
|
80
|
+
// Example: Parallel execution
|
|
81
|
+
// const [a, b, c] = await parallel([
|
|
82
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
83
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
84
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
85
|
+
// ]);
|
|
86
|
+
|
|
87
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
88
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
89
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
90
|
+
|
|
91
|
+
notify(['__WORKFLOW_NAME__', userInfo.name || userInfo + ' has been greeted!']);
|
|
92
|
+
|
|
93
|
+
console.log('Workflow completed!');
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Creating Agents
|
|
98
|
+
|
|
99
|
+
**JavaScript agent** (`agents/my-agent.js`):
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { llm } from 'agent-state-machine';
|
|
103
|
+
|
|
104
|
+
export default async function handler(context) {
|
|
105
|
+
const response = await llm(context, { model: 'smart', prompt: 'Hello!' });
|
|
106
|
+
return { greeting: response.text };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Markdown agent** (`agents/greeter.md`):
|
|
111
|
+
|
|
112
|
+
```md
|
|
113
|
+
---
|
|
114
|
+
model: fast
|
|
115
|
+
output: greeting
|
|
116
|
+
---
|
|
117
|
+
Generate a greeting for {{name}}.
|
|
118
|
+
```
|