agent-state-machine 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/bin/cli.js +30 -2
  2. package/lib/runtime/agent.js +6 -2
  3. package/lib/runtime/interaction.js +2 -1
  4. package/lib/runtime/prompt.js +37 -1
  5. package/lib/runtime/runtime.js +67 -5
  6. package/package.json +1 -1
  7. package/templates/project-builder/agents/code-fixer.md +50 -0
  8. package/templates/project-builder/agents/code-writer.md +3 -0
  9. package/templates/project-builder/agents/sanity-checker.md +6 -0
  10. package/templates/project-builder/agents/test-planner.md +3 -1
  11. package/templates/project-builder/config.js +4 -4
  12. package/templates/project-builder/scripts/workflow-helpers.js +104 -2
  13. package/templates/project-builder/workflow.js +151 -14
  14. package/templates/starter/config.js +1 -1
  15. package/vercel-server/api/submit/[token].js +0 -11
  16. package/vercel-server/local-server.js +0 -19
  17. package/vercel-server/public/remote/assets/index-BsJsLDKc.css +1 -0
  18. package/vercel-server/public/remote/assets/index-CmtT6ADh.js +168 -0
  19. package/vercel-server/public/remote/index.html +2 -2
  20. package/vercel-server/ui/src/App.jsx +69 -19
  21. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
  22. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
  23. package/vercel-server/ui/src/components/ContentCard.jsx +600 -104
  24. package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
  25. package/vercel-server/ui/src/components/Footer.jsx +9 -4
  26. package/vercel-server/ui/src/components/Header.jsx +12 -3
  27. package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
  28. package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
  29. package/vercel-server/ui/src/index.css +82 -10
  30. package/vercel-server/public/remote/assets/index-CbgeVnKw.js +0 -148
  31. 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
@@ -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));
@@ -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
- const response = await runtime.waitForInteraction(filePath, slug, targetKey);
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');
@@ -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
@@ -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
- // Local TTY input loop
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
- reject(new Error('User quit workflow'));
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",
@@ -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
  };