@arcadialdev/arcality 3.0.0 → 3.0.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.
@@ -119,7 +119,7 @@ function updateDotEnvFile(updates) {
119
119
  keys.forEach(k => { process.env[k] = updates[k]; });
120
120
  }
121
121
 
122
- function setupEnvironment() {
122
+ function setupEnvironment() {
123
123
 
124
124
  // Use config name from arcality.config or fallback
125
125
  const configName = projectConfig?.project?.name || 'Default';
@@ -133,14 +133,16 @@ function setupEnvironment() {
133
133
  process.env.CONTEXT_DIR = path.join(configDir, 'context');
134
134
  process.env.REPORTS_DIR = path.join(configDir, 'reports');
135
135
 
136
- [process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR].forEach(dir => {
137
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
138
- });
139
-
140
- // Also ensure YAML output dir from arcality.config
141
- if (projectConfig) {
142
- ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
143
- }
136
+ [process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR].forEach(dir => {
137
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
138
+ });
139
+ ensureMissionTemplates(process.env.MISSIONS_DIR);
140
+
141
+ // Also ensure YAML output dir from arcality.config
142
+ if (projectConfig) {
143
+ const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
144
+ ensureMissionTemplates(yamlOutputDir);
145
+ }
144
146
 
145
147
  // Framework detection from package.json (NO .git)
146
148
  let techStack = 'Not detected';
@@ -157,10 +159,613 @@ function setupEnvironment() {
157
159
  techStack = projectConfig.project.frameworkDetected;
158
160
  }
159
161
 
160
- return { configName, baseUrl, techStack, configDir };
161
- }
162
-
163
- function showBanner() {
162
+ return { configName, baseUrl, techStack, configDir };
163
+ }
164
+
165
+ function isYamlFile(fileName) {
166
+ return fileName.endsWith('.yaml') || fileName.endsWith('.yml');
167
+ }
168
+
169
+ function isCollectionMetaFile(fileName) {
170
+ const lower = String(fileName || '').toLowerCase();
171
+ return lower === '_collection.yaml' || lower === '_collection.yml';
172
+ }
173
+
174
+ function isSystemCollectionDir(dirName) {
175
+ return String(dirName || '').toLowerCase() === '_templates';
176
+ }
177
+
178
+ function toPosixPath(value) {
179
+ return value.split(path.sep).join('/');
180
+ }
181
+
182
+ function sanitizePathSegment(segment) {
183
+ return String(segment || '')
184
+ .trim()
185
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
186
+ .replace(/_+/g, '_')
187
+ .replace(/^_+|_+$/g, '')
188
+ .toLowerCase();
189
+ }
190
+
191
+ function sanitizeCollectionPath(input) {
192
+ const segments = String(input || '')
193
+ .split(/[\\/]+/)
194
+ .map(sanitizePathSegment)
195
+ .filter(Boolean);
196
+
197
+ return segments.join(path.sep);
198
+ }
199
+
200
+ function listYamlFilesInDir(dir) {
201
+ if (!dir || !fs.existsSync(dir)) return [];
202
+ return fs.readdirSync(dir, { withFileTypes: true })
203
+ .filter(entry => entry.isFile() && isYamlFile(entry.name) && !isCollectionMetaFile(entry.name))
204
+ .map(entry => entry.name)
205
+ .sort((a, b) => a.localeCompare(b));
206
+ }
207
+
208
+ function readYamlFile(filePath) {
209
+ try {
210
+ return loadYaml(fs.readFileSync(filePath, 'utf8'));
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function normalizeStringList(value) {
217
+ if (Array.isArray(value)) {
218
+ return value.map(v => String(v).trim()).filter(Boolean);
219
+ }
220
+ if (typeof value === 'string') {
221
+ return value.split(',').map(v => v.trim()).filter(Boolean);
222
+ }
223
+ return [];
224
+ }
225
+
226
+ function readCollectionMeta(dir, rel = '') {
227
+ const candidates = ['_collection.yaml', '_collection.yml']
228
+ .map(name => path.join(dir, name))
229
+ .filter(fs.existsSync);
230
+
231
+ if (candidates.length === 0) {
232
+ return {
233
+ label: rel ? toPosixPath(rel) : 'General (sin carpeta)',
234
+ description: '',
235
+ tags: []
236
+ };
237
+ }
238
+
239
+ const raw = readYamlFile(candidates[0]);
240
+ const meta = raw && typeof raw === 'object' ? raw : {};
241
+ return {
242
+ label: String(meta.name || meta.title || (rel ? toPosixPath(rel) : 'General (sin carpeta)')).trim(),
243
+ description: String(meta.description || '').trim(),
244
+ tags: normalizeStringList(meta.tags),
245
+ defaultPagePath: String(meta.default_page_path || meta.defaultPagePath || meta.start_at || meta.StartAt || '').trim()
246
+ };
247
+ }
248
+
249
+ function normalizeMissionSpec(rawMission, filePath = '') {
250
+ const mission = rawMission && typeof rawMission === 'object' ? rawMission : {};
251
+ const fallbackName = filePath ? path.basename(filePath, path.extname(filePath)) : 'mission';
252
+ const prompt = String(
253
+ mission.prompt ||
254
+ mission.mision ||
255
+ mission.task ||
256
+ ''
257
+ ).trim();
258
+
259
+ return {
260
+ raw: mission,
261
+ name: String(mission.name || fallbackName).trim(),
262
+ prompt,
263
+ page: String(mission.page || '').trim(),
264
+ pagePath: String(mission.page_path || mission.pagePath || '').trim(),
265
+ startAt: String(mission.start_at || mission.startAt || mission.StartAt || '').trim(),
266
+ expectedResult: String(mission.expected_result || mission.expectedResult || '').trim(),
267
+ collection: String(mission.collection || '').trim(),
268
+ suite: String(mission.suite || '').trim(),
269
+ priority: String(mission.priority || '').trim(),
270
+ tags: normalizeStringList(mission.tags),
271
+ timeout: Number.isFinite(Number(mission.timeout)) ? Number(mission.timeout) : null,
272
+ credentialId: String(mission.credential_id || mission.credentialId || '').trim()
273
+ };
274
+ }
275
+
276
+ function listMissionCollections(rootDir) {
277
+ if (!rootDir || !fs.existsSync(rootDir)) return [];
278
+
279
+ const collections = [];
280
+ const visit = (dir, rel = '') => {
281
+ const yamlFiles = listYamlFilesInDir(dir);
282
+ const meta = readCollectionMeta(dir, rel);
283
+ if (yamlFiles.length > 0) {
284
+ collections.push({
285
+ label: meta.label,
286
+ description: meta.description,
287
+ tags: meta.tags,
288
+ value: rel,
289
+ dir,
290
+ count: yamlFiles.length
291
+ });
292
+ }
293
+
294
+ const subdirs = fs.readdirSync(dir, { withFileTypes: true })
295
+ .filter(entry => entry.isDirectory() && !isSystemCollectionDir(entry.name))
296
+ .map(entry => entry.name)
297
+ .sort((a, b) => a.localeCompare(b));
298
+
299
+ for (const subdir of subdirs) {
300
+ visit(path.join(dir, subdir), rel ? path.join(rel, subdir) : subdir);
301
+ }
302
+ };
303
+
304
+ visit(rootDir);
305
+ return collections;
306
+ }
307
+
308
+ function listMissionDirectories(rootDir) {
309
+ if (!rootDir || !fs.existsSync(rootDir)) return [];
310
+
311
+ const dirs = [];
312
+ const visit = (dir, rel = '') => {
313
+ const meta = readCollectionMeta(dir, rel);
314
+ dirs.push({
315
+ label: meta.label,
316
+ description: meta.description,
317
+ tags: meta.tags,
318
+ value: rel,
319
+ dir
320
+ });
321
+
322
+ const subdirs = fs.readdirSync(dir, { withFileTypes: true })
323
+ .filter(entry => entry.isDirectory() && !isSystemCollectionDir(entry.name))
324
+ .map(entry => entry.name)
325
+ .sort((a, b) => a.localeCompare(b));
326
+
327
+ for (const subdir of subdirs) {
328
+ visit(path.join(dir, subdir), rel ? path.join(rel, subdir) : subdir);
329
+ }
330
+ };
331
+
332
+ visit(rootDir);
333
+ return dirs;
334
+ }
335
+
336
+ async function selectMissionYaml(rootDir, collectionMessage = 'Elige la carpeta de misiones:', missionMessage = 'Elige la mision guardada:') {
337
+ const collections = listMissionCollections(rootDir);
338
+ if (collections.length === 0) return null;
339
+
340
+ let collection = collections[0];
341
+ if (collections.length > 1) {
342
+ const selectedCollection = await select({
343
+ message: collectionMessage,
344
+ options: collections.map(c => ({
345
+ label: `${c.label} (${c.count})${c.description ? ` - ${c.description}` : ''}`,
346
+ value: c.value
347
+ }))
348
+ });
349
+ if (isCancel(selectedCollection)) return null;
350
+ collection = collections.find(c => c.value === selectedCollection) || collection;
351
+ }
352
+
353
+ const files = listYamlFilesInDir(collection.dir);
354
+ const selectedFile = await select({
355
+ message: missionMessage,
356
+ options: files.map(file => {
357
+ const missionSpec = normalizeMissionSpec(readYamlFile(path.join(collection.dir, file)), file);
358
+ const labelParts = [missionSpec.name || file];
359
+ if (missionSpec.priority) labelParts.push(`[${missionSpec.priority}]`);
360
+ if (missionSpec.tags.length > 0) labelParts.push(`{${missionSpec.tags.join(', ')}}`);
361
+ return { label: labelParts.join(' '), value: file };
362
+ })
363
+ });
364
+ if (isCancel(selectedFile)) return null;
365
+
366
+ return {
367
+ collection: collection.value,
368
+ file: selectedFile,
369
+ fullPath: path.join(collection.dir, selectedFile),
370
+ collectionLabel: collection.label
371
+ };
372
+ }
373
+
374
+ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
375
+ const sources = [];
376
+
377
+ const libraryCollections = listMissionCollections(libraryDir);
378
+ if (libraryCollections.length > 0) {
379
+ sources.push({
380
+ key: 'library',
381
+ label: 'Biblioteca Arcality',
382
+ description: 'Misiones archivadas por Arcality',
383
+ dir: libraryDir
384
+ });
385
+ }
386
+
387
+ const projectCollections = projectDir ? listMissionCollections(projectDir) : [];
388
+ if (projectCollections.length > 0) {
389
+ sources.push({
390
+ key: 'project',
391
+ label: 'Carpeta del proyecto',
392
+ description: 'Misiones guardadas junto al proyecto',
393
+ dir: projectDir
394
+ });
395
+ }
396
+
397
+ const rootMissionFiles = rootDir && fs.existsSync(rootDir)
398
+ ? fs.readdirSync(rootDir).filter(file => isYamlFile(file))
399
+ : [];
400
+ if (rootMissionFiles.length > 0) {
401
+ sources.push({
402
+ key: 'root',
403
+ label: 'Raiz del proyecto',
404
+ description: 'Archivos de mision ubicados en la raiz',
405
+ dir: rootDir,
406
+ files: rootMissionFiles
407
+ });
408
+ }
409
+
410
+ if (sources.length === 0) return null;
411
+
412
+ let source = sources[0];
413
+ if (sources.length > 1) {
414
+ const selectedSource = await select({
415
+ message: '¿De dónde quieres abrir la misión?',
416
+ options: sources.map(item => ({
417
+ label: `${item.label}${item.description ? ` - ${item.description}` : ''}`,
418
+ value: item.key
419
+ }))
420
+ });
421
+ if (isCancel(selectedSource)) return null;
422
+ source = sources.find(item => item.key === selectedSource) || source;
423
+ }
424
+
425
+ if (source.key === 'root') {
426
+ const selectedFile = await select({
427
+ message: 'Elige la misión guardada:',
428
+ options: source.files.map(file => ({ label: file, value: file }))
429
+ });
430
+ if (isCancel(selectedFile)) return null;
431
+
432
+ return {
433
+ source: source.label,
434
+ collection: '',
435
+ collectionLabel: source.label,
436
+ file: selectedFile,
437
+ fullPath: path.join(source.dir, selectedFile)
438
+ };
439
+ }
440
+
441
+ const selectedMission = await selectMissionYaml(
442
+ source.dir,
443
+ `Elige la carpeta de ${source.label.toLowerCase()}:`,
444
+ 'Elige la misión guardada:'
445
+ );
446
+
447
+ if (!selectedMission) return null;
448
+
449
+ return {
450
+ ...selectedMission,
451
+ source: source.label
452
+ };
453
+ }
454
+
455
+ async function selectMissionSaveDir(rootDir) {
456
+ if (!fs.existsSync(rootDir)) fs.mkdirSync(rootDir, { recursive: true });
457
+
458
+ const directories = listMissionDirectories(rootDir);
459
+ const createNewValue = '__CREATE_NEW_COLLECTION__';
460
+ const selectedDir = await select({
461
+ message: 'Carpeta para guardar la mision:',
462
+ options: [
463
+ ...directories.map(d => ({
464
+ label: `${d.label}${d.description ? ` - ${d.description}` : ''}`,
465
+ value: d.value
466
+ })),
467
+ { label: '+ Crear nueva carpeta', value: createNewValue }
468
+ ]
469
+ });
470
+
471
+ if (isCancel(selectedDir)) return null;
472
+
473
+ let relDir = selectedDir;
474
+ if (selectedDir === createNewValue) {
475
+ const newDir = await text({
476
+ message: 'Nombre de la nueva carpeta (ej., smoke/login):',
477
+ placeholder: 'regression/login',
478
+ validate: v => {
479
+ if (!v || !v.trim()) return 'El nombre de la carpeta es requerido.';
480
+ if (!sanitizeCollectionPath(v)) return 'Usa letras, numeros, guiones o diagonales.';
481
+ }
482
+ });
483
+ if (isCancel(newDir)) return null;
484
+ relDir = sanitizeCollectionPath(newDir);
485
+ }
486
+
487
+ const targetDir = relDir ? path.join(rootDir, relDir) : rootDir;
488
+ fs.mkdirSync(targetDir, { recursive: true });
489
+ return { relDir, targetDir };
490
+ }
491
+
492
+ function ensureMissionTemplates(rootDir) {
493
+ if (!rootDir) return [];
494
+ const templatesDir = path.join(rootDir, '_templates');
495
+ if (!fs.existsSync(templatesDir)) fs.mkdirSync(templatesDir, { recursive: true });
496
+
497
+ const missionTemplatePath = path.join(templatesDir, 'mission.template.yaml');
498
+ const collectionTemplatePath = path.join(templatesDir, 'collection.template.yaml');
499
+ const created = [];
500
+
501
+ const missionTemplate = `# Arcality mission template
502
+ # Use this file as a base when you want to create your own tests manually.
503
+ #
504
+ # name:
505
+ # Friendly mission name shown in future menus and reports.
506
+ name: "TS_registrar_timesheet_hoy"
507
+ #
508
+ # prompt:
509
+ # Main instruction Arcality will execute. Be explicit about the expected flow.
510
+ prompt: "Inicia sesion, abre el primer pendiente y registra un timesheet valido."
511
+ #
512
+ # page_path:
513
+ # Relative route where the mission should begin. It is resolved against BASE_URL.
514
+ page_path: "/welcome"
515
+ #
516
+ # project:
517
+ # Optional project label for traceability.
518
+ project: "MiProyectoQA"
519
+ #
520
+ # collection:
521
+ # Logical folder/category for the mission library.
522
+ collection: "timesheets/hoy"
523
+ #
524
+ # suite:
525
+ # High-level group used for future batch execution and reporting.
526
+ suite: "timesheets"
527
+ #
528
+ # tags:
529
+ # Free-form labels for filtering and organization.
530
+ tags:
531
+ - "smoke"
532
+ - "timesheets"
533
+ #
534
+ # priority:
535
+ # Suggested values: low, normal, high, critical
536
+ priority: "normal"
537
+ #
538
+ # expected_result:
539
+ # Short statement describing what proves success.
540
+ expected_result: "El registro nuevo aparece en la tabla Timesheet - Hoy."
541
+ #
542
+ # credential_id:
543
+ # Optional placeholder for future centralized credentials.
544
+ credential_id: ""
545
+ #
546
+ # hooks:
547
+ # Reserved for future before/after automation hooks.
548
+ # hooks:
549
+ # before: []
550
+ # after: []
551
+ created_at: "2026-01-01T00:00:00.000Z"
552
+ `;
553
+
554
+ const collectionTemplate = `# Arcality collection template
555
+ # Save this as _collection.yaml inside a mission folder to customize how the folder is shown.
556
+ #
557
+ # name:
558
+ # Friendly collection name displayed in menus.
559
+ name: "Timesheets diarios"
560
+ #
561
+ # description:
562
+ # One-line explanation for users choosing the collection.
563
+ description: "Misiones para registrar y validar timesheets del dia."
564
+ #
565
+ # tags:
566
+ # Optional labels inherited conceptually by the collection.
567
+ tags:
568
+ - "timesheets"
569
+ - "daily"
570
+ #
571
+ # default_page_path:
572
+ # Optional fallback route used when a mission in this folder does not define page_path.
573
+ default_page_path: "/welcome"
574
+ `;
575
+
576
+ if (!fs.existsSync(missionTemplatePath)) {
577
+ fs.writeFileSync(missionTemplatePath, missionTemplate, 'utf8');
578
+ created.push(missionTemplatePath);
579
+ }
580
+ if (!fs.existsSync(collectionTemplatePath)) {
581
+ fs.writeFileSync(collectionTemplatePath, collectionTemplate, 'utf8');
582
+ created.push(collectionTemplatePath);
583
+ }
584
+
585
+ return created;
586
+ }
587
+
588
+ async function waitForEnter(message = 'Presiona Enter para continuar...') {
589
+ if (!process.stdin.isTTY) return;
590
+
591
+ console.log(chalk.gray(`\n ${message}`));
592
+ await new Promise(resolve => {
593
+ const onKey = (key) => {
594
+ const keyStr = String(key);
595
+ if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
596
+ process.stdin.off('data', onKey);
597
+
598
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
599
+ process.stdin.setRawMode(false);
600
+ }
601
+
602
+ if (keyStr === '\u0003') {
603
+ process.exit(0);
604
+ } else {
605
+ resolve();
606
+ }
607
+ }
608
+ };
609
+
610
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
611
+ process.stdin.setRawMode(true);
612
+ }
613
+ process.stdin.resume();
614
+ process.stdin.on('data', onKey);
615
+ });
616
+ }
617
+
618
+ function getMissionStartPath(missionSpec, collectionMeta = null, fallbackPath = '') {
619
+ return missionSpec.pagePath || missionSpec.startAt || missionSpec.page || collectionMeta?.defaultPagePath || fallbackPath;
620
+ }
621
+
622
+ function listMissionLogFiles(contextDir) {
623
+ if (!contextDir || !fs.existsSync(contextDir)) return [];
624
+ return fs.readdirSync(contextDir)
625
+ .filter(file => (
626
+ (file.startsWith('agent-log-') || file.startsWith('arcality-log-')) &&
627
+ file.endsWith('.json')
628
+ ))
629
+ .sort();
630
+ }
631
+
632
+ function readLatestMissionLog(contextDir, knownFiles = new Set()) {
633
+ const files = listMissionLogFiles(contextDir);
634
+ const newFiles = files.filter(file => !knownFiles.has(file));
635
+ const targetFile = (newFiles.length > 0 ? newFiles[newFiles.length - 1] : files[files.length - 1]);
636
+ if (!targetFile) return null;
637
+
638
+ try {
639
+ const fullPath = path.join(contextDir, targetFile);
640
+ return {
641
+ file: fullPath,
642
+ data: JSON.parse(fs.readFileSync(fullPath, 'utf8'))
643
+ };
644
+ } catch {
645
+ return null;
646
+ }
647
+ }
648
+
649
+ async function runCollectionBatch({
650
+ missionsRootDir,
651
+ selectedCollection = null,
652
+ debug = false
653
+ }) {
654
+ const collections = listMissionCollections(missionsRootDir);
655
+ if (collections.length === 0) {
656
+ note(chalk.yellow('No hay colecciones con misiones guardadas para ejecutar.'), 'Run All');
657
+ return;
658
+ }
659
+
660
+ let targetCollections = collections;
661
+ if (selectedCollection && selectedCollection !== '__ALL__') {
662
+ targetCollections = collections.filter(c => c.value === selectedCollection);
663
+ }
664
+
665
+ if (targetCollections.length === 0) {
666
+ note(chalk.yellow('La coleccion solicitada no existe o no contiene misiones.'), 'Run All');
667
+ return;
668
+ }
669
+
670
+ const missionsToRun = [];
671
+ for (const collection of targetCollections) {
672
+ const files = listYamlFilesInDir(collection.dir);
673
+ for (const file of files) {
674
+ missionsToRun.push({
675
+ collection,
676
+ file,
677
+ fullPath: path.join(collection.dir, file)
678
+ });
679
+ }
680
+ }
681
+
682
+ if (missionsToRun.length === 0) {
683
+ note(chalk.yellow('No se encontraron misiones ejecutables en la seleccion actual.'), 'Run All');
684
+ return;
685
+ }
686
+
687
+ const sBatch = spinner();
688
+ if (!debug) {
689
+ sBatch.start(`Ejecutando ${missionsToRun.length} misiones de ${targetCollections.length} coleccion(es)...`);
690
+ }
691
+
692
+ const summary = [];
693
+ for (let index = 0; index < missionsToRun.length; index++) {
694
+ const missionEntry = missionsToRun[index];
695
+ const rawMission = readYamlFile(missionEntry.fullPath);
696
+ const missionSpec = normalizeMissionSpec(rawMission, missionEntry.fullPath);
697
+ const collectionMeta = readCollectionMeta(missionEntry.collection.dir, missionEntry.collection.value);
698
+ const discoverPath = getMissionStartPath(missionSpec, collectionMeta, '/');
699
+ const contextDir = process.env.CONTEXT_DIR;
700
+ const knownLogFiles = new Set(listMissionLogFiles(contextDir));
701
+
702
+ if (!missionSpec.prompt) {
703
+ summary.push({
704
+ name: missionSpec.name || missionEntry.file,
705
+ collection: missionEntry.collection.label,
706
+ success: false,
707
+ reason: 'missing_prompt'
708
+ });
709
+ continue;
710
+ }
711
+
712
+ if (!debug) {
713
+ sBatch.message(`[${index + 1}/${missionsToRun.length}] ${missionSpec.name} (${missionEntry.collection.label})`);
714
+ } else {
715
+ console.log(chalk.cyan(`[RUN-ALL] ${index + 1}/${missionsToRun.length} -> ${missionSpec.name} [${missionEntry.collection.label}]`));
716
+ }
717
+
718
+ try {
719
+ await run(process.execPath, [
720
+ path.join(PROJECT_ROOT, 'scripts', 'gen-and-run.mjs'),
721
+ '--agent',
722
+ '--batch',
723
+ '--skip-save-prompt',
724
+ `--discover=${discoverPath}`,
725
+ missionSpec.prompt
726
+ ], {
727
+ cwd: ORIGINAL_CWD,
728
+ env: {
729
+ ...process.env,
730
+ ARCALITY_ROOT: PROJECT_ROOT,
731
+ ARCALITY_BATCH_MODE: 'true'
732
+ }
733
+ });
734
+
735
+ const latestLog = readLatestMissionLog(contextDir, knownLogFiles);
736
+ const success = latestLog?.data?.success === true;
737
+ summary.push({
738
+ name: missionSpec.name || missionEntry.file,
739
+ collection: missionEntry.collection.label,
740
+ success,
741
+ reason: latestLog?.data?.fail_reason || null
742
+ });
743
+ } catch (error) {
744
+ summary.push({
745
+ name: missionSpec.name || missionEntry.file,
746
+ collection: missionEntry.collection.label,
747
+ success: false,
748
+ reason: error instanceof Error ? error.message : 'runner_error'
749
+ });
750
+ }
751
+ }
752
+
753
+ const passed = summary.filter(item => item.success).length;
754
+ const failed = summary.length - passed;
755
+
756
+ if (!debug) {
757
+ sBatch.stop(chalk.green(`Coleccion ejecutada. Exitosas: ${passed} | Fallidas: ${failed}`));
758
+ }
759
+
760
+ const lines = summary.map(item => {
761
+ const status = item.success ? 'PASS' : 'FAIL';
762
+ const reason = item.reason ? ` (${item.reason})` : '';
763
+ return `- [${status}] ${item.collection} -> ${item.name}${reason}`;
764
+ });
765
+ note(lines.join('\n'), 'Resumen Run All');
766
+ }
767
+
768
+ function showBanner() {
164
769
  console.clear();
165
770
 
166
771
  let version = 'unknown';
@@ -440,24 +1045,47 @@ async function main() {
440
1045
  initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
441
1046
  }
442
1047
 
443
- const argv = process.argv.slice(2);
444
- let initialDiscoverPath = null;
445
- let initialSmartMode = false;
446
- let initialAgentMode = false;
447
-
448
- const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
449
- if (dIndex !== -1) {
450
- const token = argv[dIndex];
451
- if (token.includes('=')) initialDiscoverPath = token.split('=')[1];
452
- else if (argv[dIndex + 1]) { initialDiscoverPath = argv[dIndex + 1]; argv.splice(dIndex + 1, 1); }
453
- argv.splice(dIndex, 1);
454
- }
455
- const sIndex = argv.findIndex(a => a === '--smart');
456
- if (sIndex !== -1) { initialSmartMode = true; argv.splice(sIndex, 1); }
457
- const aIndex = argv.findIndex(a => a === '--agent');
458
- if (aIndex !== -1) { initialAgentMode = true; argv.splice(aIndex, 1); }
459
- const debugIndex = argv.findIndex(a => a === '--debug');
460
- if (debugIndex !== -1) { argv.splice(debugIndex, 1); } // Handled by run() naturally now
1048
+ const argv = process.argv.slice(2);
1049
+ let initialDiscoverPath = null;
1050
+ let initialSmartMode = false;
1051
+ let initialAgentMode = false;
1052
+ let initialRunAllMode = false;
1053
+ let initialTemplateMode = false;
1054
+ let initialSkipSavePrompt = process.env.ARCALITY_SKIP_SAVE_PROMPT === 'true';
1055
+ let initialBatchMode = process.env.ARCALITY_BATCH_MODE === 'true';
1056
+ let initialCollectionPath = null;
1057
+
1058
+ const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
1059
+ if (dIndex !== -1) {
1060
+ const token = argv[dIndex];
1061
+ if (token.includes('=')) initialDiscoverPath = token.split('=')[1];
1062
+ else if (argv[dIndex + 1]) { initialDiscoverPath = argv[dIndex + 1]; argv.splice(dIndex + 1, 1); }
1063
+ argv.splice(dIndex, 1);
1064
+ }
1065
+ const sIndex = argv.findIndex(a => a === '--smart');
1066
+ if (sIndex !== -1) { initialSmartMode = true; argv.splice(sIndex, 1); }
1067
+ const aIndex = argv.findIndex(a => a === '--agent');
1068
+ if (aIndex !== -1) { initialAgentMode = true; argv.splice(aIndex, 1); }
1069
+ const runAllIndex = argv.findIndex(a => a === '--run-all');
1070
+ if (runAllIndex !== -1) { initialRunAllMode = true; argv.splice(runAllIndex, 1); }
1071
+ const templateIndex = argv.findIndex(a => a === 'template' || a === '--template');
1072
+ if (templateIndex !== -1) { initialTemplateMode = true; argv.splice(templateIndex, 1); }
1073
+ const skipSaveIndex = argv.findIndex(a => a === '--skip-save-prompt');
1074
+ if (skipSaveIndex !== -1) { initialSkipSavePrompt = true; argv.splice(skipSaveIndex, 1); }
1075
+ const batchIndex = argv.findIndex(a => a === '--batch');
1076
+ if (batchIndex !== -1) { initialBatchMode = true; argv.splice(batchIndex, 1); }
1077
+ const collectionIndex = argv.findIndex(a => a === '--collection' || a.startsWith('--collection='));
1078
+ if (collectionIndex !== -1) {
1079
+ const token = argv[collectionIndex];
1080
+ if (token.includes('=')) initialCollectionPath = sanitizeCollectionPath(token.split('=')[1]);
1081
+ else if (argv[collectionIndex + 1]) {
1082
+ initialCollectionPath = sanitizeCollectionPath(argv[collectionIndex + 1]);
1083
+ argv.splice(collectionIndex + 1, 1);
1084
+ }
1085
+ argv.splice(collectionIndex, 1);
1086
+ }
1087
+ const debugIndex = argv.findIndex(a => a === '--debug');
1088
+ if (debugIndex !== -1) { argv.splice(debugIndex, 1); } // Handled by run() naturally now
461
1089
 
462
1090
  const promptArg = argv.join(" ").trim();
463
1091
  let firstRun = true;
@@ -466,59 +1094,116 @@ async function main() {
466
1094
  setupEnvironment();
467
1095
 
468
1096
  while (true) {
469
- let prompt = firstRun ? promptArg : "";
470
- let agentMode = firstRun ? initialAgentMode : false;
471
- let smartMode = firstRun ? initialSmartMode : false;
472
- let discoverPath = firstRun ? initialDiscoverPath : null;
473
-
474
- let skipMenu = (firstRun && (prompt || agentMode || smartMode)) || agentMode;
475
- if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
476
-
477
- firstRun = false;
478
-
479
- if (!skipMenu) {
480
- showBanner();
481
- try {
1097
+ let prompt = firstRun ? promptArg : "";
1098
+ let agentMode = firstRun ? initialAgentMode : false;
1099
+ let smartMode = firstRun ? initialSmartMode : false;
1100
+ let discoverPath = firstRun ? initialDiscoverPath : null;
1101
+ let runAllMode = firstRun ? initialRunAllMode : false;
1102
+ let templateMode = firstRun ? initialTemplateMode : false;
1103
+ let collectionPath = firstRun ? initialCollectionPath : null;
1104
+ let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
1105
+ let batchMode = firstRun ? initialBatchMode : false;
1106
+
1107
+ let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
1108
+ if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
1109
+
1110
+ firstRun = false;
1111
+
1112
+ if (!skipMenu) {
1113
+ showBanner();
1114
+ try {
482
1115
  // ── Build menu options (single config mode) ──
483
1116
  const missionsDir = process.env.MISSIONS_DIR;
484
- const savedMissions = fs.existsSync(missionsDir)
485
- ? fs.readdirSync(missionsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
486
- : [];
1117
+ const savedMissionCollections = listMissionCollections(missionsDir);
487
1118
 
488
1119
  // Check for YAML files in the yaml output dir from arcality.config
489
- let yamlOutputFiles = [];
1120
+ let projectMissionCollections = [];
490
1121
  if (projectConfig) {
491
1122
  const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
492
1123
  if (fs.existsSync(yamlDir)) {
493
- yamlOutputFiles = fs.readdirSync(yamlDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
1124
+ projectMissionCollections = listMissionCollections(yamlDir);
494
1125
  }
495
1126
  }
496
1127
 
497
1128
  // Root-level YAML templates
498
- const rootYamlFiles = fs.readdirSync(PROJECT_ROOT).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
499
-
500
- const options = [
501
- { label: '◉ Desplegar Arcality (Prompt)', value: 'agent' },
502
- ...(savedMissions.length ? [{ label: '⟳ Lanzar Misión Archivada (.yaml)', value: 'launch_saved' }] : []),
503
- ...(yamlOutputFiles.length ? [{ label: '⟶ Activar Modo GUÍA (Reusar YAML)', value: 'reuse_yaml' }] : []),
504
- ...(rootYamlFiles.length ? [{ label: '📂 Cargar Matriz de Patrones (YAML Raíz)', value: 'yaml_list' }] : []),
505
- { label: '⚙ Recalibrar Sistema (init)', value: 'reconfigure' },
506
- { label: ' Terminar Proceso', value: 'exit' }
507
- ];
508
-
509
- const action = await select({
1129
+ const rootMissionFiles = fs.existsSync(ORIGINAL_CWD)
1130
+ ? fs.readdirSync(ORIGINAL_CWD).filter(f => isYamlFile(f))
1131
+ : [];
1132
+ const hasSavedMissionSources = savedMissionCollections.length || projectMissionCollections.length || rootMissionFiles.length;
1133
+ const yamlOutputCollections = projectMissionCollections;
1134
+ const rootYamlFiles = rootMissionFiles;
1135
+
1136
+ const options = [
1137
+ { label: ' Desplegar Arcality (Prompt)', value: 'agent' },
1138
+ ...(savedMissionCollections.length ? [{ label: '▶ Ejecutar colección completa', value: 'run_collection' }] : []),
1139
+ ...(savedMissionCollections.length ? [{ label: '⟳ Lanzar Misión Archivada (.yaml)', value: 'launch_saved' }] : []),
1140
+ ...(yamlOutputCollections.length ? [{ label: '⟶ Activar Modo GUÍA (Reusar YAML)', value: 'reuse_yaml' }] : []),
1141
+ ...(rootYamlFiles.length ? [{ label: '📂 Cargar Matriz de Patrones (YAML Raíz)', value: 'yaml_list' }] : []),
1142
+ { label: '📄 Generar templates YAML', value: 'generate_templates' },
1143
+ { label: '◎ Recalibrar Sistema (init)', value: 'reconfigure' },
1144
+ { label: '✕ Terminar Proceso', value: 'exit' }
1145
+ ];
1146
+
1147
+ const menuOptions = options
1148
+ .filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
1149
+ .map(option => option.value === 'generate_templates'
1150
+ ? { ...option, label: '◫ Generar plantillas de misión' }
1151
+ : option
1152
+ );
1153
+
1154
+ if (hasSavedMissionSources) {
1155
+ const insertIndex = Math.min(menuOptions.length, 2);
1156
+ menuOptions.splice(insertIndex, 0, {
1157
+ label: '◌ Abrir misión guardada',
1158
+ value: 'open_saved_mission'
1159
+ });
1160
+ }
1161
+
1162
+ const action = await select({
510
1163
  message: '¿Qué te gustaría hacer?',
511
- options
512
- });
1164
+ options: menuOptions
1165
+ });
513
1166
 
514
1167
  if (isCancel(action) || action === 'exit') {
515
1168
  outro(chalk.cyan('¡Hasta luego!'));
516
1169
  break;
517
1170
  }
518
1171
 
519
- if (action === 'agent') {
520
- agentMode = true;
521
- } else if (action === 'reconfigure') {
1172
+ if (action === 'agent') {
1173
+ agentMode = true;
1174
+ } else if (action === 'run_collection') {
1175
+ runAllMode = true;
1176
+ } else if (action === 'open_saved_mission') {
1177
+ const projectDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1178
+ const selectedMission = await selectSavedMissionSource({
1179
+ libraryDir: missionsDir,
1180
+ projectDir,
1181
+ rootDir: ORIGINAL_CWD
1182
+ });
1183
+ if (!selectedMission) continue;
1184
+
1185
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1186
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1187
+ prompt = missionSpec.prompt || yamlContent;
1188
+ discoverPath = getMissionStartPath(
1189
+ missionSpec,
1190
+ readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
1191
+ );
1192
+ agentMode = true;
1193
+ } else if (action === 'generate_templates') {
1194
+ const createdMain = ensureMissionTemplates(missionsDir);
1195
+ const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1196
+ const createdSecondary = yamlDir ? ensureMissionTemplates(yamlDir) : [];
1197
+ const createdFiles = [...createdMain, ...createdSecondary];
1198
+ note(
1199
+ createdFiles.length > 0
1200
+ ? chalk.green(`Templates creados:\n${createdFiles.join('\n')}`)
1201
+ : chalk.cyan(`Los templates ya existen en ${path.join(missionsDir, '_templates')}`),
1202
+ 'Templates Arcality'
1203
+ );
1204
+ await waitForEnter('Presiona Enter para volver al menu...');
1205
+ continue;
1206
+ } else if (action === 'reconfigure') {
522
1207
  // Launch arcality init in the user's project directory
523
1208
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
524
1209
  try {
@@ -546,49 +1231,101 @@ async function main() {
546
1231
  await new Promise(r => setTimeout(r, 3000));
547
1232
  }
548
1233
  continue;
549
- } else if (action === 'launch_saved') {
550
- const sel = await select({
551
- message: 'Elige la misión guardada:',
552
- options: savedMissions.map(f => ({ label: f, value: f }))
553
- });
554
- if (isCancel(sel)) continue;
555
-
556
- const yamlContent = fs.readFileSync(path.join(missionsDir, sel), 'utf8');
557
- const data = loadYaml(yamlContent);
558
- prompt = data.prompt || data.mision || yamlContent;
559
- agentMode = true;
560
- } else if (action === 'reuse_yaml') {
561
- const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
562
- const sel = await select({
563
- message: 'Elige el YAML a reusar:',
564
- options: yamlOutputFiles.map(f => ({ label: f, value: f }))
1234
+ } else if (action === 'launch_saved') {
1235
+ const selectedMission = await selectMissionYaml(
1236
+ missionsDir,
1237
+ 'Elige la carpeta de misiones:',
1238
+ 'Elige la mision guardada:'
1239
+ );
1240
+ if (!selectedMission) continue;
1241
+
1242
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1243
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1244
+ prompt = missionSpec.prompt || yamlContent;
1245
+ discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1246
+ agentMode = true;
1247
+ } else if (action === 'reuse_yaml') {
1248
+ const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
1249
+ const selectedMission = await selectMissionYaml(
1250
+ yamlDir,
1251
+ 'Elige la carpeta de YAML:',
1252
+ 'Elige el YAML a reusar:'
1253
+ );
1254
+ if (!selectedMission) continue;
1255
+
1256
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1257
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1258
+ prompt = missionSpec.prompt || yamlContent;
1259
+ discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1260
+ agentMode = true;
1261
+ } else if (action === 'yaml_list') {
1262
+ const sel = await select({
1263
+ message: 'Elige el archivo YAML:',
1264
+ options: rootYamlFiles.map(f => ({ label: f, value: f }))
565
1265
  });
566
1266
  if (isCancel(sel)) continue;
567
-
568
- const yamlContent = fs.readFileSync(path.join(yamlDir, sel), 'utf8');
569
- const data = loadYaml(yamlContent);
570
- prompt = data.prompt || data.mision || yamlContent;
571
- agentMode = true;
572
- } else if (action === 'yaml_list') {
573
- const sel = await select({
574
- message: 'Elige el archivo YAML:',
575
- options: rootYamlFiles.map(f => ({ label: f, value: f }))
576
- });
577
- if (isCancel(sel)) continue;
578
-
579
- const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
580
- const data = loadYaml(yamlContent);
581
- prompt = data.mision || data.prompt || yamlContent;
582
- agentMode = true;
583
- }
1267
+
1268
+ const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
1269
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
1270
+ prompt = missionSpec.prompt || yamlContent;
1271
+ discoverPath = getMissionStartPath(missionSpec);
1272
+ agentMode = true;
1273
+ }
584
1274
  } catch (e) {
585
1275
  console.error("Menu error:", e);
586
1276
  break;
587
- }
588
- }
589
-
590
- // --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
591
- if (agentMode) {
1277
+ }
1278
+ }
1279
+
1280
+ if (templateMode) {
1281
+ const createdMain = ensureMissionTemplates(process.env.MISSIONS_DIR);
1282
+ const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1283
+ const createdSecondary = yamlDir ? ensureMissionTemplates(yamlDir) : [];
1284
+ const createdFiles = [...createdMain, ...createdSecondary];
1285
+ note(
1286
+ createdFiles.length > 0
1287
+ ? chalk.green(`Templates creados:\n${createdFiles.join('\n')}`)
1288
+ : chalk.cyan(`Los templates ya existen en ${path.join(process.env.MISSIONS_DIR, '_templates')}`),
1289
+ 'Templates Arcality'
1290
+ );
1291
+
1292
+ if (skipMenu) return;
1293
+ continue;
1294
+ }
1295
+
1296
+ if (runAllMode) {
1297
+ let batchCollection = collectionPath;
1298
+ if (!batchCollection && process.stdin.isTTY) {
1299
+ const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
1300
+ const selectedBatchCollection = await select({
1301
+ message: 'Elige la colección a ejecutar:',
1302
+ options: [
1303
+ { label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
1304
+ ...availableCollections.map(collection => ({
1305
+ label: `${collection.label} (${collection.count})${collection.description ? ` - ${collection.description}` : ''}`,
1306
+ value: collection.value
1307
+ }))
1308
+ ]
1309
+ });
1310
+ if (isCancel(selectedBatchCollection)) {
1311
+ if (skipMenu) return;
1312
+ continue;
1313
+ }
1314
+ batchCollection = selectedBatchCollection;
1315
+ }
1316
+
1317
+ await runCollectionBatch({
1318
+ missionsRootDir: process.env.MISSIONS_DIR,
1319
+ selectedCollection: batchCollection || '__ALL__',
1320
+ debug: process.argv.includes('--debug')
1321
+ });
1322
+
1323
+ if (skipMenu) return;
1324
+ continue;
1325
+ }
1326
+
1327
+ // --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
1328
+ if (agentMode) {
592
1329
  if (!prompt || prompt.trim().length < 4) {
593
1330
  const p = await text({
594
1331
  message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
@@ -1026,36 +1763,66 @@ async function main() {
1026
1763
  // ── Ping Project ──
1027
1764
  await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
1028
1765
 
1029
- // ── Save Mission YAML ──
1030
- const saveDecision = await select({
1031
- message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
1032
- options: [
1033
- { label: ' Sí, guardar como YAML', value: 'yes' },
1034
- { label: '❌ No, gracias', value: 'no' }
1035
- ]
1036
- });
1037
-
1038
- if (saveDecision === 'yes') {
1766
+ // ── Save Mission YAML ──
1767
+ let saveDecision = 'no';
1768
+ if (!skipSavePrompt && !batchMode) {
1769
+ saveDecision = await select({
1770
+ message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
1771
+ options: [
1772
+ { label: '✅ Sí, guardar como YAML', value: 'yes' },
1773
+ { label: '❌ No, gracias', value: 'no' }
1774
+ ]
1775
+ });
1776
+ }
1777
+
1778
+ if (saveDecision === 'yes') {
1039
1779
  const name = await text({
1040
1780
  message: 'Nombre de la misión (ej., login_valido):',
1041
1781
  placeholder: 'my_mission',
1042
1782
  validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
1043
1783
  });
1044
1784
 
1045
- if (!isCancel(name)) {
1046
- const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
1785
+ if (!isCancel(name)) {
1786
+ const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
1047
1787
 
1048
1788
  // Save to missions dir (existing flow)
1049
1789
  const missionsDir = process.env.MISSIONS_DIR;
1050
- const yamlPathMissions = path.join(missionsDir, `${safeName}.yaml`);
1051
-
1052
- const safeProjectName = projectConfig?.project?.name || 'Default';
1053
- let yamlData = `name: ${JSON.stringify(name)}\nprompt: ${JSON.stringify(prompt)}\nproject: ${JSON.stringify(safeProjectName)}\ncreated_at: ${JSON.stringify(new Date().toISOString())}\n`;
1790
+ const missionSaveDir = await selectMissionSaveDir(missionsDir);
1791
+ if (!missionSaveDir) {
1792
+ note(chalk.yellow('Guardado de mision cancelado.'), 'Persistencia Arcality');
1793
+ continue;
1794
+ }
1795
+ const yamlPathMissions = path.join(missionSaveDir.targetDir, `${safeName}.yaml`);
1796
+
1797
+ const safeProjectName = projectConfig?.project?.name || 'Default';
1798
+ const collectionValue = missionSaveDir.relDir ? toPosixPath(missionSaveDir.relDir) : '';
1799
+ const suiteValue = missionSaveDir.relDir ? toPosixPath(missionSaveDir.relDir).split('/')[0] : '';
1800
+ let yamlData = [
1801
+ `name: ${JSON.stringify(name)}`,
1802
+ `prompt: ${JSON.stringify(prompt)}`,
1803
+ `project: ${JSON.stringify(safeProjectName)}`,
1804
+ ...(collectionValue ? [`collection: ${JSON.stringify(collectionValue)}`] : []),
1805
+ ...(suiteValue ? [`suite: ${JSON.stringify(suiteValue)}`] : []),
1806
+ 'tags: []',
1807
+ `priority: ${JSON.stringify('normal')}`,
1808
+ `created_at: ${JSON.stringify(new Date().toISOString())}`
1809
+ ].join('\n') + '\n';
1054
1810
 
1055
1811
  const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
1056
- if (fs.existsSync(smartYamlPath)) {
1057
- yamlData = fs.readFileSync(smartYamlPath, 'utf8');
1058
- }
1812
+ if (fs.existsSync(smartYamlPath)) {
1813
+ const smartYamlContent = fs.readFileSync(smartYamlPath, 'utf8').trim();
1814
+ yamlData = [
1815
+ `name: ${JSON.stringify(name)}`,
1816
+ `prompt: ${JSON.stringify(prompt)}`,
1817
+ `project: ${JSON.stringify(safeProjectName)}`,
1818
+ ...(collectionValue ? [`collection: ${JSON.stringify(collectionValue)}`] : []),
1819
+ ...(suiteValue ? [`suite: ${JSON.stringify(suiteValue)}`] : []),
1820
+ 'tags: []',
1821
+ `priority: ${JSON.stringify('normal')}`,
1822
+ `created_at: ${JSON.stringify(new Date().toISOString())}`,
1823
+ smartYamlContent
1824
+ ].join('\n') + '\n';
1825
+ }
1059
1826
 
1060
1827
  // Internal fallback backup for the agent
1061
1828
  fs.writeFileSync(yamlPathMissions, yamlData);
@@ -1063,7 +1830,9 @@ async function main() {
1063
1830
  // Primary save to user's specified directory from arcality.config
1064
1831
  if (projectConfig) {
1065
1832
  const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
1066
- const yamlPathOutput = path.join(yamlOutputDir, `${safeName}.yaml`);
1833
+ const yamlOutputTargetDir = missionSaveDir.relDir ? path.join(yamlOutputDir, missionSaveDir.relDir) : yamlOutputDir;
1834
+ fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
1835
+ const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
1067
1836
  fs.writeFileSync(yamlPathOutput, yamlData);
1068
1837
  note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
1069
1838
  } else {
@@ -1121,8 +1890,11 @@ async function main() {
1121
1890
  try {
1122
1891
  const ctxDir = process.env.CONTEXT_DIR;
1123
1892
  if (fs.existsSync(ctxDir)) {
1124
- const files = fs.readdirSync(ctxDir);
1125
- const logFiles = files.filter(f => f.startsWith('arcality-log-') && f.endsWith('.json')).sort();
1893
+ const files = fs.readdirSync(ctxDir);
1894
+ const logFiles = files.filter(f =>
1895
+ (f.startsWith('arcality-log-') || f.startsWith('agent-log-')) &&
1896
+ f.endsWith('.json')
1897
+ ).sort();
1126
1898
  if (logFiles.length > 0) {
1127
1899
  const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
1128
1900
  const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
@@ -1193,16 +1965,16 @@ async function main() {
1193
1965
  globalReportProcess = null;
1194
1966
  }
1195
1967
 
1196
- if (fs.existsSync(arcalityPath)) {
1197
- console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
1198
- if (process.platform === "win32") {
1199
- exec(`start "" "${arcalityPath}"`);
1200
- } else {
1201
- exec(`open "${arcalityPath}"`);
1202
- }
1203
- } else {
1204
- console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
1205
- }
1968
+ if (!batchMode && fs.existsSync(arcalityPath)) {
1969
+ console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
1970
+ if (process.platform === "win32") {
1971
+ exec(`start "" "${arcalityPath}"`);
1972
+ } else {
1973
+ exec(`open "${arcalityPath}"`);
1974
+ }
1975
+ } else if (!fs.existsSync(arcalityPath)) {
1976
+ console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
1977
+ }
1206
1978
 
1207
1979
  await new Promise(resolve => setTimeout(resolve, 2000));
1208
1980
 
@@ -1216,46 +1988,48 @@ async function main() {
1216
1988
  process.removeListener('SIGINT', postProcessSigint);
1217
1989
  process.removeListener('SIGTERM', postProcessSigint);
1218
1990
 
1219
- // Esperar Enter para regresar al menú
1220
- // Usamos escucha directa de stdin (raw mode) porque @clack/text
1221
- // no procesa Enter correctamente cuando setRawMode(true) está activo.
1222
- console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
1223
- await new Promise(resolve => {
1224
- const onKey = (key) => {
1225
- const keyStr = String(key);
1226
- if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
1227
- process.stdin.off('data', onKey);
1228
-
1229
- // Restaurar stdin en modo normal para @clack/prompts
1230
- if (process.stdin.isTTY && process.stdin.setRawMode) {
1231
- process.stdin.setRawMode(false);
1232
- }
1233
- // FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
1234
- // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
1235
- if (skipMenu) {
1236
- process.stdin.pause();
1237
- }
1238
-
1239
- if (keyStr === '\u0003') {
1240
- process.exit(0); // Ctrl+C = salir inmediatamente
1241
- } else {
1242
- resolve();
1243
- }
1244
- }
1245
- };
1246
-
1247
- // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
1248
- // al terminar cada select()/text(). En Node.js, agregar un listener 'data'
1249
- // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
1250
- // Resultado: el event loop queda vacío y el proceso termina antes de que
1251
- // el usuario pueda presionar Enter.
1252
- // Solución: llamar resume() explícitamente antes de registrar el listener.
1253
- if (process.stdin.isTTY && process.stdin.setRawMode) {
1254
- process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
1255
- }
1256
- process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
1257
- process.stdin.on('data', onKey);
1258
- });
1991
+ if (!batchMode) {
1992
+ // Esperar Enter para regresar al menú
1993
+ // Usamos escucha directa de stdin (raw mode) porque @clack/text
1994
+ // no procesa Enter correctamente cuando setRawMode(true) está activo.
1995
+ console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
1996
+ await new Promise(resolve => {
1997
+ const onKey = (key) => {
1998
+ const keyStr = String(key);
1999
+ if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
2000
+ process.stdin.off('data', onKey);
2001
+
2002
+ // Restaurar stdin en modo normal para @clack/prompts
2003
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2004
+ process.stdin.setRawMode(false);
2005
+ }
2006
+ // FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
2007
+ // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
2008
+ if (skipMenu) {
2009
+ process.stdin.pause();
2010
+ }
2011
+
2012
+ if (keyStr === '\u0003') {
2013
+ process.exit(0); // Ctrl+C = salir inmediatamente
2014
+ } else {
2015
+ resolve();
2016
+ }
2017
+ }
2018
+ };
2019
+
2020
+ // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2021
+ // al terminar cada select()/text(). En Node.js, agregar un listener 'data'
2022
+ // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
2023
+ // Resultado: el event loop queda vacío y el proceso termina antes de que
2024
+ // el usuario pueda presionar Enter.
2025
+ // Solución: llamar resume() explícitamente antes de registrar el listener.
2026
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2027
+ process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
2028
+ }
2029
+ process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
2030
+ process.stdin.on('data', onKey);
2031
+ });
2032
+ }
1259
2033
 
1260
2034
  if (skipMenu) {
1261
2035
  outro(chalk.cyan('¡Misión terminada! Hasta luego.'));