agent-state-machine 2.2.1 → 2.3.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 +33 -5
- package/lib/file-tree.js +1 -1
- package/lib/runtime/agent.js +6 -2
- package/lib/runtime/interaction.js +2 -1
- package/lib/runtime/prompt.js +37 -1
- package/lib/runtime/runtime.js +67 -5
- package/lib/setup.js +4 -4
- package/package.json +1 -1
- package/templates/project-builder/agents/code-fixer.md +50 -0
- package/templates/project-builder/agents/code-writer.md +3 -0
- package/templates/project-builder/agents/sanity-checker.md +6 -0
- package/templates/project-builder/agents/test-planner.md +3 -1
- package/templates/project-builder/config.js +4 -4
- package/templates/project-builder/scripts/workflow-helpers.js +104 -2
- package/templates/project-builder/workflow.js +151 -14
- package/templates/starter/config.js +1 -1
- package/vercel-server/api/submit/[token].js +0 -11
- package/vercel-server/local-server.js +0 -19
- package/vercel-server/public/remote/assets/index-BTLc1QSv.js +168 -0
- package/vercel-server/public/remote/assets/index-DLa4X08t.css +1 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/App.jsx +53 -18
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
- package/vercel-server/ui/src/components/ContentCard.jsx +607 -103
- package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
- package/vercel-server/ui/src/components/Footer.jsx +9 -4
- package/vercel-server/ui/src/components/Header.jsx +12 -3
- package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
- package/vercel-server/ui/src/index.css +82 -10
- package/vercel-server/public/remote/assets/index-CbgeVnKw.js +0 -148
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
package/bin/cli.js
CHANGED
|
@@ -97,6 +97,8 @@ Options:
|
|
|
97
97
|
--template, -t Template name for --setup (default: starter)
|
|
98
98
|
--local, -l Use local server instead of remote (starts on localhost:3000)
|
|
99
99
|
--new, -n Generate a new remote follow path
|
|
100
|
+
--full-auto, -a Auto-select first option for choice interactions (no blocking)
|
|
101
|
+
--delay, -d Seconds to wait before auto-select in full-auto mode (default: 20)
|
|
100
102
|
-reset Reset workflow state before running
|
|
101
103
|
-reset-hard Hard reset workflow before running
|
|
102
104
|
--help, -h Show help
|
|
@@ -106,7 +108,7 @@ Environment Variables:
|
|
|
106
108
|
STATE_MACHINE_REMOTE_URL Override the default remote server URL (for local dev testing)
|
|
107
109
|
|
|
108
110
|
Workflow Structure:
|
|
109
|
-
workflows/<name>/
|
|
111
|
+
.workflows/<name>/
|
|
110
112
|
├── workflow.js # Native JS workflow (async/await)
|
|
111
113
|
├── config.js # Model/API key configuration
|
|
112
114
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
@@ -140,7 +142,7 @@ async function confirmHardReset(workflowName) {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
function workflowsRoot() {
|
|
143
|
-
return path.join(process.cwd(), 'workflows');
|
|
145
|
+
return path.join(process.cwd(), '.workflows');
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
function resolveWorkflowDir(workflowName) {
|
|
@@ -192,7 +194,7 @@ function listWorkflows() {
|
|
|
192
194
|
const root = workflowsRoot();
|
|
193
195
|
|
|
194
196
|
if (!fs.existsSync(root)) {
|
|
195
|
-
console.log('No workflows directory found.');
|
|
197
|
+
console.log('No .workflows directory found.');
|
|
196
198
|
console.log('Run `state-machine --setup <name>` to create your first workflow.');
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
@@ -238,7 +240,9 @@ async function runOrResume(
|
|
|
238
240
|
useLocalServer = false,
|
|
239
241
|
forceNewRemotePath = false,
|
|
240
242
|
preReset = false,
|
|
241
|
-
preResetHard = false
|
|
243
|
+
preResetHard = false,
|
|
244
|
+
fullAuto = false,
|
|
245
|
+
autoSelectDelay = null
|
|
242
246
|
} = {}
|
|
243
247
|
) {
|
|
244
248
|
const workflowDir = resolveWorkflowDir(workflowName);
|
|
@@ -294,6 +298,16 @@ async function runOrResume(
|
|
|
294
298
|
await runtime.enableRemote(remoteUrl, { sessionToken, uiBaseUrl: useLocalServer });
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
// Set full-auto mode from CLI flag (will be merged with config.js during runWorkflow)
|
|
302
|
+
if (fullAuto) {
|
|
303
|
+
runtime.workflowConfig.fullAuto = true;
|
|
304
|
+
if (autoSelectDelay !== null) {
|
|
305
|
+
runtime.workflowConfig.autoSelectDelay = autoSelectDelay;
|
|
306
|
+
}
|
|
307
|
+
const delay = runtime.workflowConfig.autoSelectDelay;
|
|
308
|
+
console.log(`\n\x1b[36m\x1b[1m⚡ Full-auto mode enabled\x1b[0m - Agent will auto-select recommended options after ${delay}s countdown`);
|
|
309
|
+
}
|
|
310
|
+
|
|
297
311
|
// Prevent system sleep while workflow runs (macOS only)
|
|
298
312
|
// Display can still sleep, but system stays awake for remote follow
|
|
299
313
|
const stopCaffeinate = preventSleep();
|
|
@@ -370,14 +384,28 @@ async function main() {
|
|
|
370
384
|
const forceNewRemotePath = args.includes('--new') || args.includes('-n');
|
|
371
385
|
const preReset = args.includes('-reset');
|
|
372
386
|
const preResetHard = args.includes('-reset-hard');
|
|
387
|
+
const fullAuto = args.includes('--full-auto') || args.includes('-a');
|
|
373
388
|
const remoteEnabled = !useLocalServer; // Use Vercel if not local
|
|
389
|
+
|
|
390
|
+
// Parse --delay or -d flag
|
|
391
|
+
let autoSelectDelay = null;
|
|
392
|
+
const delayFlagIndex = args.findIndex((arg) => arg === '--delay' || arg === '-d');
|
|
393
|
+
if (delayFlagIndex !== -1 && args[delayFlagIndex + 1]) {
|
|
394
|
+
const delayValue = parseInt(args[delayFlagIndex + 1], 10);
|
|
395
|
+
if (!isNaN(delayValue) && delayValue >= 0) {
|
|
396
|
+
autoSelectDelay = delayValue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
374
400
|
try {
|
|
375
401
|
await runOrResume(workflowName, {
|
|
376
402
|
remoteEnabled,
|
|
377
403
|
useLocalServer,
|
|
378
404
|
forceNewRemotePath,
|
|
379
405
|
preReset,
|
|
380
|
-
preResetHard
|
|
406
|
+
preResetHard,
|
|
407
|
+
fullAuto,
|
|
408
|
+
autoSelectDelay
|
|
381
409
|
});
|
|
382
410
|
} catch (err) {
|
|
383
411
|
console.error('Error:', err.message || String(err));
|
package/lib/file-tree.js
CHANGED
package/lib/runtime/agent.js
CHANGED
|
@@ -533,7 +533,10 @@ ${content}
|
|
|
533
533
|
validation: interaction.validation,
|
|
534
534
|
confirmLabel: interaction.confirmLabel,
|
|
535
535
|
cancelLabel: interaction.cancelLabel,
|
|
536
|
-
context: interaction.context
|
|
536
|
+
context: interaction.context,
|
|
537
|
+
// Include full-auto info for remote UI countdown
|
|
538
|
+
fullAuto: runtime.workflowConfig.fullAuto || false,
|
|
539
|
+
autoSelectDelay: runtime.workflowConfig.autoSelectDelay ?? 20
|
|
537
540
|
});
|
|
538
541
|
|
|
539
542
|
if (effectiveAgentName) {
|
|
@@ -541,7 +544,8 @@ ${content}
|
|
|
541
544
|
}
|
|
542
545
|
|
|
543
546
|
// Block and wait for user input (instead of throwing)
|
|
544
|
-
|
|
547
|
+
// Pass the full interaction object for full-auto mode support
|
|
548
|
+
const response = await runtime.waitForInteraction(filePath, slug, targetKey, interaction);
|
|
545
549
|
|
|
546
550
|
return response;
|
|
547
551
|
}
|
|
@@ -117,8 +117,9 @@ export function formatInteractionPrompt(interaction) {
|
|
|
117
117
|
interaction.options.forEach((opt, index) => {
|
|
118
118
|
const letter = String.fromCharCode(65 + index);
|
|
119
119
|
const label = opt.label || opt.key || `Option ${index + 1}`;
|
|
120
|
+
const recommended = index === 0 ? ' (Recommended)' : '';
|
|
120
121
|
const desc = opt.description ? ` - ${opt.description}` : '';
|
|
121
|
-
lines.push(`- ${letter}: ${label}${desc}`);
|
|
122
|
+
lines.push(`- ${letter}: ${label}${recommended}${desc}`);
|
|
122
123
|
});
|
|
123
124
|
if (interaction.allowCustom) {
|
|
124
125
|
lines.push('- Other: Provide a custom response');
|
package/lib/runtime/prompt.js
CHANGED
|
@@ -51,9 +51,45 @@ export async function askHuman(question, options = {}) {
|
|
|
51
51
|
validation: interaction?.validation,
|
|
52
52
|
confirmLabel: interaction?.confirmLabel,
|
|
53
53
|
cancelLabel: interaction?.cancelLabel,
|
|
54
|
-
context: interaction?.context
|
|
54
|
+
context: interaction?.context,
|
|
55
|
+
// Include full-auto info for remote UI countdown
|
|
56
|
+
fullAuto: runtime.workflowConfig.fullAuto || false,
|
|
57
|
+
autoSelectDelay: runtime.workflowConfig.autoSelectDelay ?? 20
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
// Full-auto mode: show countdown and auto-select first option for choice interactions
|
|
61
|
+
if (runtime.workflowConfig.fullAuto && interaction?.type === 'choice') {
|
|
62
|
+
const options = interaction.options || [];
|
|
63
|
+
if (options.length > 0) {
|
|
64
|
+
const firstOption = options[0];
|
|
65
|
+
const autoResponse = firstOption.key || firstOption.label;
|
|
66
|
+
const delay = runtime.workflowConfig.autoSelectDelay ?? 20;
|
|
67
|
+
|
|
68
|
+
console.log(`\n${C.cyan}${C.bold}${interaction.prompt || 'Choice required'}${C.reset}`);
|
|
69
|
+
if (runtime.remoteEnabled && runtime.remoteUrl) {
|
|
70
|
+
console.log(`${C.dim}(Remote: ${runtime.remoteUrl})${C.reset}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Countdown timer
|
|
74
|
+
for (let i = delay; i > 0; i--) {
|
|
75
|
+
process.stdout.write(`\r${C.yellow}⚡ Agent deciding for you in ${i}...${C.reset} `);
|
|
76
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
77
|
+
}
|
|
78
|
+
console.log(`\r${C.green}${C.bold}⚡ Auto-selected: ${autoResponse}${C.reset} \n`);
|
|
79
|
+
|
|
80
|
+
runtime._rawMemory[memoryKey] = autoResponse;
|
|
81
|
+
runtime.persist();
|
|
82
|
+
|
|
83
|
+
await runtime.prependHistory({
|
|
84
|
+
event: 'PROMPT_AUTO_ANSWERED',
|
|
85
|
+
slug,
|
|
86
|
+
autoSelected: autoResponse
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return autoResponse;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
// Check if we're in TTY mode (interactive terminal)
|
|
58
94
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
59
95
|
// Interactive mode - prompt directly, with remote support
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -80,7 +80,11 @@ export class WorkflowRuntime {
|
|
|
80
80
|
projectRoot: path.resolve(workflowDir, '../..'),
|
|
81
81
|
fileTracking: true,
|
|
82
82
|
fileTrackingIgnore: DEFAULT_IGNORE,
|
|
83
|
-
fileTrackingKeepDeleted: false
|
|
83
|
+
fileTrackingKeepDeleted: false,
|
|
84
|
+
// Full-auto mode (auto-select first option for choice interactions)
|
|
85
|
+
fullAuto: false,
|
|
86
|
+
maxQuickFixAttempts: 10,
|
|
87
|
+
autoSelectDelay: 20 // seconds before auto-selecting in full-auto mode
|
|
84
88
|
};
|
|
85
89
|
|
|
86
90
|
// Load steering
|
|
@@ -320,6 +324,8 @@ export class WorkflowRuntime {
|
|
|
320
324
|
configUrl.searchParams.set('t', Date.now().toString());
|
|
321
325
|
const configModule = await import(configUrl.href);
|
|
322
326
|
const cfg = configModule.config || configModule.default || {};
|
|
327
|
+
// Preserve CLI-set fullAuto (it takes precedence over config.js)
|
|
328
|
+
const cliFullAuto = this.workflowConfig.fullAuto;
|
|
323
329
|
this.workflowConfig = {
|
|
324
330
|
models: cfg.models || {},
|
|
325
331
|
apiKeys: cfg.apiKeys || {},
|
|
@@ -328,7 +334,11 @@ export class WorkflowRuntime {
|
|
|
328
334
|
projectRoot: cfg.projectRoot || path.resolve(this.workflowDir, '../..'),
|
|
329
335
|
fileTracking: cfg.fileTracking ?? true,
|
|
330
336
|
fileTrackingIgnore: cfg.fileTrackingIgnore || DEFAULT_IGNORE,
|
|
331
|
-
fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false
|
|
337
|
+
fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false,
|
|
338
|
+
// Full-auto mode: CLI flag takes precedence, then config.js, then default false
|
|
339
|
+
fullAuto: cliFullAuto || cfg.fullAuto || false,
|
|
340
|
+
maxQuickFixAttempts: cfg.maxQuickFixAttempts ?? 10,
|
|
341
|
+
autoSelectDelay: cfg.autoSelectDelay ?? this.workflowConfig.autoSelectDelay // seconds before auto-selecting
|
|
332
342
|
};
|
|
333
343
|
|
|
334
344
|
// Import workflow module
|
|
@@ -368,8 +378,17 @@ export class WorkflowRuntime {
|
|
|
368
378
|
/**
|
|
369
379
|
* Wait for user to confirm interaction is complete, then return the response
|
|
370
380
|
* Supports both local TTY input and remote browser responses
|
|
381
|
+
* @param {string} filePath - Path to the interaction file
|
|
382
|
+
* @param {string} slug - Interaction slug
|
|
383
|
+
* @param {string} targetKey - Memory key to store response
|
|
384
|
+
* @param {object} [interaction] - Optional interaction object for full-auto mode
|
|
371
385
|
*/
|
|
372
|
-
async waitForInteraction(filePath, slug, targetKey) {
|
|
386
|
+
async waitForInteraction(filePath, slug, targetKey, interaction = null) {
|
|
387
|
+
// Determine if we're in full-auto mode for choice interactions
|
|
388
|
+
const isFullAutoChoice = this.workflowConfig.fullAuto && interaction?.type === 'choice' && interaction.options?.length > 0;
|
|
389
|
+
const autoResponse = isFullAutoChoice ? (interaction.options[0].key || interaction.options[0].label) : null;
|
|
390
|
+
const delay = isFullAutoChoice ? (this.workflowConfig.autoSelectDelay ?? 20) : 0;
|
|
391
|
+
|
|
373
392
|
console.log(`\n${C.yellow}${C.bold}⏸ Interaction required.${C.reset}`);
|
|
374
393
|
|
|
375
394
|
if (this.remoteEnabled && this.remoteUrl) {
|
|
@@ -387,11 +406,16 @@ export class WorkflowRuntime {
|
|
|
387
406
|
return new Promise((resolve, reject) => {
|
|
388
407
|
// Track if we've already resolved (to prevent double-resolution)
|
|
389
408
|
let resolved = false;
|
|
409
|
+
let countdownTimer = null;
|
|
390
410
|
|
|
391
411
|
const cleanup = () => {
|
|
392
412
|
resolved = true;
|
|
393
413
|
rl.close();
|
|
394
414
|
this.pendingRemoteInteraction = null;
|
|
415
|
+
if (countdownTimer) {
|
|
416
|
+
clearTimeout(countdownTimer);
|
|
417
|
+
countdownTimer = null;
|
|
418
|
+
}
|
|
395
419
|
};
|
|
396
420
|
|
|
397
421
|
// Set up remote interaction listener
|
|
@@ -414,7 +438,9 @@ export class WorkflowRuntime {
|
|
|
414
438
|
source: 'remote'
|
|
415
439
|
});
|
|
416
440
|
|
|
441
|
+
const displayResponse = typeof response === 'object' ? JSON.stringify(response) : response;
|
|
417
442
|
console.log(`\n${C.green}✓ Interaction resolved (remote): ${slug}${C.reset}`);
|
|
443
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${displayResponse}`);
|
|
418
444
|
resolve(response);
|
|
419
445
|
},
|
|
420
446
|
reject: (err) => {
|
|
@@ -425,7 +451,38 @@ export class WorkflowRuntime {
|
|
|
425
451
|
};
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
//
|
|
454
|
+
// Full-auto countdown (can be interrupted by remote submission or local input)
|
|
455
|
+
if (isFullAutoChoice) {
|
|
456
|
+
let remaining = delay;
|
|
457
|
+
const tick = () => {
|
|
458
|
+
if (resolved) return;
|
|
459
|
+
if (remaining <= 0) {
|
|
460
|
+
// Auto-select
|
|
461
|
+
cleanup();
|
|
462
|
+
console.log(`\r ${C.cyan}${C.bold}⚡ Auto-selected: ${autoResponse}${C.reset} `);
|
|
463
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${autoResponse}`);
|
|
464
|
+
|
|
465
|
+
this._rawMemory[targetKey] = autoResponse;
|
|
466
|
+
this.persist();
|
|
467
|
+
|
|
468
|
+
this.prependHistory({
|
|
469
|
+
event: 'INTERACTION_AUTO_RESOLVED',
|
|
470
|
+
slug,
|
|
471
|
+
targetKey,
|
|
472
|
+
autoSelected: autoResponse
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
resolve(autoResponse);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
process.stdout.write(`\r ${C.cyan}${C.bold}⚡ Agent deciding for you in ${remaining}...${C.reset} `);
|
|
479
|
+
remaining--;
|
|
480
|
+
countdownTimer = setTimeout(tick, 1000);
|
|
481
|
+
};
|
|
482
|
+
tick();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Local TTY input loop (always active - can interrupt countdown in full-auto mode)
|
|
429
486
|
const ask = () => {
|
|
430
487
|
if (resolved) return;
|
|
431
488
|
|
|
@@ -438,13 +495,18 @@ export class WorkflowRuntime {
|
|
|
438
495
|
// Read and return the response from file
|
|
439
496
|
try {
|
|
440
497
|
const response = await this.readInteractionResponse(filePath, slug, targetKey);
|
|
498
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${typeof response === 'object' ? JSON.stringify(response) : response}`);
|
|
441
499
|
resolve(response);
|
|
442
500
|
} catch (err) {
|
|
443
501
|
reject(err);
|
|
444
502
|
}
|
|
445
503
|
} else if (a === 'q') {
|
|
446
504
|
cleanup();
|
|
447
|
-
|
|
505
|
+
console.log(`\n${C.yellow}⏹ Workflow stopped by user${C.reset}`);
|
|
506
|
+
this.status = 'STOPPED';
|
|
507
|
+
this.persist();
|
|
508
|
+
this.prependHistory({ event: 'WORKFLOW_STOPPED', reason: 'user_quit' });
|
|
509
|
+
process.exit(0);
|
|
448
510
|
} else {
|
|
449
511
|
ask();
|
|
450
512
|
}
|
package/lib/setup.js
CHANGED
|
@@ -70,7 +70,7 @@ function copyTemplateDir(srcDir, destDir, replacements, createdPaths) {
|
|
|
70
70
|
* Setup a new workflow with directory structure
|
|
71
71
|
*/
|
|
72
72
|
async function setup(workflowName, options = {}) {
|
|
73
|
-
const workflowsDir = path.join(process.cwd(), 'workflows');
|
|
73
|
+
const workflowsDir = path.join(process.cwd(), '.workflows');
|
|
74
74
|
const workflowDir = path.join(workflowsDir, workflowName);
|
|
75
75
|
const templateName = options.template || DEFAULT_TEMPLATE;
|
|
76
76
|
|
|
@@ -111,9 +111,9 @@ async function setup(workflowName, options = {}) {
|
|
|
111
111
|
console.log('─'.repeat(40));
|
|
112
112
|
console.log(`\n✓ Workflow '${workflowName}' created successfully!\n`);
|
|
113
113
|
console.log('Next steps:');
|
|
114
|
-
console.log(` 1. Edit workflows/${workflowName}/workflow.js to implement your flow`);
|
|
115
|
-
console.log(` 2. Edit workflows/${workflowName}/config.js to set models/API keys`);
|
|
116
|
-
console.log(` 3. Add custom agents in workflows/${workflowName}/agents/`);
|
|
114
|
+
console.log(` 1. Edit .workflows/${workflowName}/workflow.js to implement your flow`);
|
|
115
|
+
console.log(` 2. Edit .workflows/${workflowName}/config.js to set models/API keys`);
|
|
116
|
+
console.log(` 3. Add custom agents in .workflows/${workflowName}/agents/`);
|
|
117
117
|
console.log(` 4. Run: state-machine run ${workflowName}\n`);
|
|
118
118
|
}
|
|
119
119
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: high
|
|
3
|
+
format: json
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Fixer Agent
|
|
7
|
+
|
|
8
|
+
You fix specific issues in existing code based on sanity check failures.
|
|
9
|
+
|
|
10
|
+
## Critical Guidelines
|
|
11
|
+
|
|
12
|
+
**DO NOT** disable, skip, or remove failing tests to make them pass.
|
|
13
|
+
Your fixes must address the actual underlying code issues that cause tests to fail.
|
|
14
|
+
|
|
15
|
+
- ❌ Never add `.skip()`, `.todo()`, or comment out tests
|
|
16
|
+
- ❌ Never modify test expectations to match broken behavior
|
|
17
|
+
- ❌ Never delete test files or test cases
|
|
18
|
+
- ❌ Never wrap tests in `try/catch` to swallow errors
|
|
19
|
+
- ✅ Fix the implementation code to pass existing tests
|
|
20
|
+
- ✅ Fix test setup/teardown issues if the tests themselves are misconfigured
|
|
21
|
+
- ✅ Update tests ONLY if the original requirements were misunderstood
|
|
22
|
+
|
|
23
|
+
If the issue truly cannot be fixed within the current architecture, set `"confidence": "low"` and explain why in the analysis.
|
|
24
|
+
|
|
25
|
+
## Input
|
|
26
|
+
- task: Task definition
|
|
27
|
+
- originalImplementation: Current code-writer output
|
|
28
|
+
- sanityCheckResults: Failed checks with specific errors
|
|
29
|
+
- testPlan: Test plan for context
|
|
30
|
+
- previousAttempts: Number of quick-fix attempts so far
|
|
31
|
+
|
|
32
|
+
## Output Format
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"analysis": {
|
|
36
|
+
"rootCauses": ["What caused each failure"],
|
|
37
|
+
"fixApproach": "Strategy for fixing"
|
|
38
|
+
},
|
|
39
|
+
"fixes": [
|
|
40
|
+
{
|
|
41
|
+
"path": "src/feature.js",
|
|
42
|
+
"operation": "replace",
|
|
43
|
+
"code": "// Full corrected file content"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"expectedResolutions": ["Which checks should now pass"],
|
|
47
|
+
"confidence": "high|medium|low"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Focus on minimal, targeted fixes. Don't rewrite entire files unless necessary.
|
|
@@ -27,6 +27,9 @@ Implement the task following these principles:
|
|
|
27
27
|
- Implement to satisfy the test plan
|
|
28
28
|
- Ensure all test cases can pass
|
|
29
29
|
- Consider edge cases identified in testing
|
|
30
|
+
- Write runnable test files, not just descriptions
|
|
31
|
+
- Use appropriate test locations (e.g. *.test.js, *.spec.js, __tests__/)
|
|
32
|
+
- Tests must import and exercise real implementation functions
|
|
30
33
|
|
|
31
34
|
## Output Format
|
|
32
35
|
|
|
@@ -9,6 +9,7 @@ Input:
|
|
|
9
9
|
- task: { title, description, doneDefinition, sanityCheck }
|
|
10
10
|
- implementation: code-writer output
|
|
11
11
|
- testPlan: test-planner output
|
|
12
|
+
- testFramework: { framework, command }
|
|
12
13
|
|
|
13
14
|
Return JSON only in this shape:
|
|
14
15
|
{
|
|
@@ -34,6 +35,8 @@ Guidelines:
|
|
|
34
35
|
- If the task describes a server endpoint, include a curl check.
|
|
35
36
|
- Keep checks short, clear, and runnable.
|
|
36
37
|
- Include at least one file_exists or file_contains check when files are created/modified.
|
|
38
|
+
- If tests exist (from testPlan or implementation), include a type "test_suite" check.
|
|
39
|
+
- Use testFramework.command for running tests (optionally target specific files when possible).
|
|
37
40
|
|
|
38
41
|
Task:
|
|
39
42
|
{{task}}
|
|
@@ -43,3 +46,6 @@ Implementation:
|
|
|
43
46
|
|
|
44
47
|
Test Plan:
|
|
45
48
|
{{testPlan}}
|
|
49
|
+
|
|
50
|
+
Test Framework:
|
|
51
|
+
{{testFramework}}
|
|
@@ -22,6 +22,7 @@ Create a comprehensive test plan for the task. Include:
|
|
|
22
22
|
- Cover happy path and error cases
|
|
23
23
|
- Include tests for security concerns flagged in review
|
|
24
24
|
- Prioritize tests by risk and importance
|
|
25
|
+
- Provide expected test file paths for planned tests
|
|
25
26
|
|
|
26
27
|
## Output Format
|
|
27
28
|
|
|
@@ -59,7 +60,8 @@ Return a valid JSON object:
|
|
|
59
60
|
"scenario": "Empty input handling",
|
|
60
61
|
"expectedBehavior": "Return validation error"
|
|
61
62
|
}
|
|
62
|
-
]
|
|
63
|
+
],
|
|
64
|
+
"testFilePaths": ["src/feature.test.js"]
|
|
63
65
|
},
|
|
64
66
|
"testingNotes": "Any special considerations or setup needed"
|
|
65
67
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
models: {
|
|
3
|
-
fast: "gemini",
|
|
4
|
-
low: "gemini",
|
|
5
|
-
med: "gemini",
|
|
6
|
-
high: "gemini",
|
|
3
|
+
fast: "gemini -m gemini-2.5-flash-lite",
|
|
4
|
+
low: "gemini -m gemini-2.5-flash-lite",
|
|
5
|
+
med: "gemini -m gemini-2.5-flash-lite",
|
|
6
|
+
high: "gemini -m gemini-2.5-flash-lite",
|
|
7
7
|
},
|
|
8
8
|
apiKeys: {
|
|
9
9
|
gemini: process.env.GEMINI_API_KEY,
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { memory } from 'agent-state-machine';
|
|
3
|
+
import { memory, getCurrentRuntime } from 'agent-state-machine';
|
|
4
|
+
|
|
5
|
+
// Write implementation files from code-writer agent output
|
|
6
|
+
function writeImplementationFiles(implementation) {
|
|
7
|
+
const runtime = getCurrentRuntime();
|
|
8
|
+
if (!runtime) {
|
|
9
|
+
throw new Error('writeImplementationFiles must be called within a workflow context');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const projectRoot = runtime.workflowConfig.projectRoot;
|
|
13
|
+
const files = implementation?.implementation?.files || implementation?.files || [];
|
|
14
|
+
const written = [];
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
if (!file.path || !file.code) {
|
|
18
|
+
console.warn(` [File] Skipping invalid file entry: ${JSON.stringify(file)}`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fullPath = path.resolve(projectRoot, file.path);
|
|
23
|
+
|
|
24
|
+
// Ensure directory exists
|
|
25
|
+
const dir = path.dirname(fullPath);
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(fullPath, file.code);
|
|
31
|
+
written.push(file.path);
|
|
32
|
+
console.log(` [File] Created: ${file.path}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return written;
|
|
36
|
+
}
|
|
4
37
|
|
|
5
38
|
// Write markdown file to workflow state directory
|
|
6
39
|
function writeMarkdownFile(stateDir, filename, content) {
|
|
@@ -109,8 +142,72 @@ function setTaskData(phaseIndex, taskId, dataKey, value) {
|
|
|
109
142
|
memory[key] = value;
|
|
110
143
|
}
|
|
111
144
|
|
|
145
|
+
function clearPartialTaskData(phaseIndex, taskId, keepKeys = []) {
|
|
146
|
+
const allKeys = [
|
|
147
|
+
'security_pre',
|
|
148
|
+
'tests',
|
|
149
|
+
'code',
|
|
150
|
+
'review',
|
|
151
|
+
'security_post',
|
|
152
|
+
'sanity_checks',
|
|
153
|
+
'sanity_results'
|
|
154
|
+
];
|
|
155
|
+
for (const key of allKeys) {
|
|
156
|
+
if (!keepKeys.includes(key)) {
|
|
157
|
+
setTaskData(phaseIndex, taskId, key, null);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getQuickFixAttempts(phaseIndex, taskId) {
|
|
163
|
+
return getTaskData(phaseIndex, taskId, 'quick_fix_attempts') || 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function incrementQuickFixAttempts(phaseIndex, taskId) {
|
|
167
|
+
const current = getQuickFixAttempts(phaseIndex, taskId);
|
|
168
|
+
setTaskData(phaseIndex, taskId, 'quick_fix_attempts', current + 1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resetQuickFixAttempts(phaseIndex, taskId) {
|
|
172
|
+
setTaskData(phaseIndex, taskId, 'quick_fix_attempts', 0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function detectTestFramework() {
|
|
176
|
+
const runtime = getCurrentRuntime();
|
|
177
|
+
const projectRoot = runtime?.workflowConfig?.projectRoot || process.cwd();
|
|
178
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(pkgPath)) {
|
|
181
|
+
return { framework: 'vitest', command: 'npx vitest run', isDefault: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let pkg;
|
|
185
|
+
try {
|
|
186
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.warn(` [Test] Failed to parse package.json: ${error.message}`);
|
|
189
|
+
return { framework: 'vitest', command: 'npx vitest run', isDefault: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
193
|
+
const testScript = pkg.scripts?.test || '';
|
|
194
|
+
|
|
195
|
+
if (testScript.includes('vitest') || deps.vitest) {
|
|
196
|
+
return { framework: 'vitest', command: 'npm test' };
|
|
197
|
+
}
|
|
198
|
+
if (testScript.includes('jest') || deps.jest) {
|
|
199
|
+
return { framework: 'jest', command: 'npm test' };
|
|
200
|
+
}
|
|
201
|
+
if (testScript.includes('mocha') || deps.mocha) {
|
|
202
|
+
return { framework: 'mocha', command: 'npm test' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { framework: 'vitest', command: 'npx vitest run', isDefault: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
112
208
|
export {
|
|
113
209
|
writeMarkdownFile,
|
|
210
|
+
writeImplementationFiles,
|
|
114
211
|
isApproval,
|
|
115
212
|
renderRoadmapMarkdown,
|
|
116
213
|
renderTasksMarkdown,
|
|
@@ -118,5 +215,10 @@ export {
|
|
|
118
215
|
getTaskStage,
|
|
119
216
|
setTaskStage,
|
|
120
217
|
getTaskData,
|
|
121
|
-
setTaskData
|
|
218
|
+
setTaskData,
|
|
219
|
+
clearPartialTaskData,
|
|
220
|
+
getQuickFixAttempts,
|
|
221
|
+
incrementQuickFixAttempts,
|
|
222
|
+
resetQuickFixAttempts,
|
|
223
|
+
detectTestFramework
|
|
122
224
|
};
|