openprompt-lang 1.2.7 → 1.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.
@@ -0,0 +1,707 @@
1
+ // @kind(module)
2
+ // @contract(in: taskDescription:string, options:object -> out: {workflowId:string, label:string, steps:array, confidence:number})
3
+ // @limit(lines: 610)
4
+ // @pattern(provider)
5
+ // @deps(@external: [path, fs])
6
+
7
+ /**
8
+ * Selector inteligente de workflows.
9
+ *
10
+ * Analiza la descripción de una tarea y selecciona el workflow más adecuado
11
+ * basado en matching de keywords contra los triggers definidos en workflows.json.
12
+ *
13
+ * Soporta:
14
+ * - Matching por keywords exactas y parciales
15
+ * - Puntaje de confianza (0-1) basado en cobertura de triggers
16
+ * - Detección de tipo de tarea (feature, bugfix, refactor, docs, research)
17
+ * - Fallback inteligente cuando no hay match perfecto
18
+ * - Detección de conflictos con planes existentes
19
+ * - Detección de tickets existentes que se solapan
20
+ *
21
+ * Uso:
22
+ * import { selectWorkflow } from './selector.js';
23
+ * const match = selectWorkflow("Implementar tabla de reportes con filtros");
24
+ * console.log(match.label); // "Nueva Feature"
25
+ * match.steps.forEach(s => console.log(`${s.step}. ${s.action}`));
26
+ */
27
+
28
+ import { readFileSync, existsSync, readdirSync } from "fs"
29
+ import { join } from "path"
30
+
31
+ /**
32
+ * @typedef {object} WorkflowMatch
33
+ * @property {string} workflowId - ID del workflow seleccionado
34
+ * @property {string} label - Nombre legible del workflow
35
+ * @property {string} icon - Icono del workflow
36
+ * @property {Array} steps - Pasos a seguir (renumerados)
37
+ * @property {number} confidence - Confianza del match (0-1)
38
+ * @property {string[]} [knowledge_refs] - Referencias de conocimiento recomendadas
39
+ * @property {string[]} [tips] - Tips del workflow
40
+ * @property {string} taskType - Tipo detectado de tarea
41
+ * @property {boolean} requiresTicket - Si requiere ticket antes de implementar
42
+ * @property {boolean} requiresDocs - Si requiere actualizar docs al cerrar
43
+ * @property {boolean} requiresPlan - Si requiere plan antes de implementar
44
+ * @property {Array} [pendingAdjustments] - Ajustes pendientes de planes existentes
45
+ * @property {Array} [existingTickets] - Tickets existentes que se solapan
46
+ */
47
+
48
+ /**
49
+ * @typedef {object} Workflow
50
+ * @property {string} label
51
+ * @property {string} icon
52
+ * @property {string[]} trigger
53
+ * @property {Array} steps
54
+ * @property {string[]} [knowledge_refs]
55
+ * @property {string[]} [tips]
56
+ */
57
+
58
+ /** Tipos de tarea detectables con sus requerimientos */
59
+ const TASK_TYPES = Object.freeze({
60
+ bugfix: { requiresTicket: true, requiresDocs: true, requiresPlan: false },
61
+ feature: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
62
+ refactor: { requiresTicket: true, requiresDocs: false, requiresPlan: true },
63
+ docs: { requiresTicket: false, requiresDocs: true, requiresPlan: false },
64
+ research: { requiresTicket: false, requiresDocs: false, requiresPlan: false },
65
+ enhancement: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
66
+ config: { requiresTicket: false, requiresDocs: true, requiresPlan: false },
67
+ unknown: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
68
+ })
69
+
70
+ /** Keywords para detectar tipo de tarea */
71
+ const TYPE_PATTERNS = Object.freeze({
72
+ bugfix: [
73
+ "bug",
74
+ "error",
75
+ "fallo",
76
+ "crash",
77
+ "exception",
78
+ "no funciona",
79
+ "fix",
80
+ "arreglar",
81
+ "bugfix",
82
+ "hotfix",
83
+ "issue",
84
+ "problema",
85
+ "incorrecto",
86
+ "mal",
87
+ "roto",
88
+ "stack trace",
89
+ "traceback",
90
+ "unexpected",
91
+ "fails",
92
+ "falla",
93
+ ],
94
+ feature: [
95
+ "implementar",
96
+ "agregar",
97
+ "nuev",
98
+ "feature",
99
+ "crear",
100
+ "añadir",
101
+ "soporte para",
102
+ "función",
103
+ "funcionalidad",
104
+ "modulo",
105
+ "módulo",
106
+ "componente",
107
+ "página",
108
+ "vista",
109
+ "integrar",
110
+ "desarrollar",
111
+ "construir",
112
+ "hacer",
113
+ ],
114
+ refactor: [
115
+ "refactor",
116
+ "reorganizar",
117
+ "limpiar",
118
+ "optimizar",
119
+ "mejorar rendimiento",
120
+ "reestructurar",
121
+ "simplificar",
122
+ "modularizar",
123
+ "extraer",
124
+ "separar",
125
+ "reordenar",
126
+ "renombrar",
127
+ "dividir",
128
+ "consolidar",
129
+ "unificar",
130
+ ],
131
+ docs: [
132
+ "documentar",
133
+ "readme",
134
+ "documentación",
135
+ "doc",
136
+ "mermaid",
137
+ "diagrama",
138
+ "wiki",
139
+ "api doc",
140
+ "swagger",
141
+ "manual",
142
+ "guía",
143
+ "tutorial",
144
+ "ejemplo",
145
+ "comment",
146
+ "comentario",
147
+ "explicación",
148
+ ],
149
+ research: [
150
+ "investigar",
151
+ "explorar",
152
+ "analizar",
153
+ "evaluar",
154
+ "comparar",
155
+ "research",
156
+ "spike",
157
+ "proof of concept",
158
+ "poc",
159
+ "viabilidad",
160
+ "factibilidad",
161
+ "estudio",
162
+ "diagnóstico",
163
+ ],
164
+ enhancement: [
165
+ "mejorar",
166
+ "enhance",
167
+ "optimizar",
168
+ "actualizar",
169
+ "upgrade",
170
+ "migrar",
171
+ "modernizar",
172
+ "adaptar",
173
+ "ampliar",
174
+ "extender",
175
+ "añadir funcionalidad",
176
+ "nueva capacidad",
177
+ "más rápido",
178
+ ],
179
+ config: [
180
+ "configurar",
181
+ "setup",
182
+ "instalar",
183
+ "desplegar",
184
+ "deploy",
185
+ "config",
186
+ "environ",
187
+ "variable de entorno",
188
+ "ci/cd",
189
+ "docker",
190
+ "compilar",
191
+ "build",
192
+ "empaquetar",
193
+ "distribuir",
194
+ ],
195
+ })
196
+
197
+ /**
198
+ * Carga los workflows desde workflows.json.
199
+ *
200
+ * @returns {object} Diccionario de workflows
201
+ */
202
+ function loadWorkflows() {
203
+ const paths = [
204
+ join(process.cwd(), ".opencode", "workflows.json"),
205
+ join(process.cwd(), "..", ".opencode", "workflows.json"),
206
+ join(import.meta.dirname, "..", "..", "..", ".opencode", "workflows.json"),
207
+ ]
208
+
209
+ for (const p of paths) {
210
+ if (existsSync(p)) {
211
+ try {
212
+ const raw = readFileSync(p, "utf-8")
213
+ const parsed = JSON.parse(raw)
214
+ return parsed.workflows || parsed
215
+ } catch (err) {
216
+ console.error(`[selector] Error cargando workflows desde ${p}:`, err.message)
217
+ return getDefaultWorkflows()
218
+ }
219
+ }
220
+ }
221
+
222
+ return getDefaultWorkflows()
223
+ }
224
+
225
+ /**
226
+ * Workflows por defecto cuando no existe workflows.json.
227
+ *
228
+ * @returns {object}
229
+ */
230
+ function getDefaultWorkflows() {
231
+ return {
232
+ feature: {
233
+ label: "Nueva Feature",
234
+ icon: "🚀",
235
+ trigger: ["implementar", "feature", "nuevo", "crear", "agregar", "soporte"],
236
+ steps: [
237
+ { step: 1, action: "analyze_project", purpose: "Analizar estructura actual" },
238
+ {
239
+ step: 2,
240
+ action: "context_unified",
241
+ query: "{task}",
242
+ purpose: "Buscar contexto relevante",
243
+ },
244
+ { step: 3, action: "work_context_plan", purpose: "Planificar implementación" },
245
+ {
246
+ step: 4,
247
+ action: "implement",
248
+ rules: ["Anotaciones OPL primero", "Named exports", "Máx 300 líneas"],
249
+ },
250
+ { step: 5, action: "validate", purpose: "Verificar anotaciones" },
251
+ { step: 6, action: "generate_tests", purpose: "Tests de regresión" },
252
+ { step: 7, action: "docs_updated", purpose: "Actualizar documentación" },
253
+ ],
254
+ knowledge_refs: [],
255
+ tips: ["Siempre comenzar con anotaciones OPL antes de implementar lógica"],
256
+ },
257
+ bugfix: {
258
+ label: "Corrección de Bug",
259
+ icon: "🐛",
260
+ trigger: ["bug", "error", "fix", "fallo", "no funciona"],
261
+ steps: [
262
+ { step: 1, action: "recall", query: "error {task}", purpose: "Buscar errores previos" },
263
+ { step: 2, action: "lint_file", purpose: "Verificar anotaciones del archivo" },
264
+ { step: 3, action: "fix", rules: ["@learn-error", "Nunca as para silenciar errores"] },
265
+ { step: 4, action: "generate_tests", purpose: "Tests de regresión" },
266
+ { step: 5, action: "validate", purpose: "Verificar fix" },
267
+ ],
268
+ knowledge_refs: [],
269
+ tips: ["Documentar con @learn-error para que la IA aprenda del error"],
270
+ },
271
+ refactor: {
272
+ label: "Refactorización",
273
+ icon: "🔧",
274
+ trigger: ["refactor", "reorganizar", "limpiar", "optimizar"],
275
+ steps: [
276
+ { step: 1, action: "extract", purpose: "Analizar patrones reutilizables" },
277
+ { step: 2, action: "validate", purpose: "Verificar anotaciones actuales" },
278
+ { step: 3, action: "work_context_plan", purpose: "Planificar refactor" },
279
+ {
280
+ step: 4,
281
+ action: "implement",
282
+ rules: ["Máx 300 líneas", "Named exports", "@limit actualizado"],
283
+ },
284
+ { step: 5, action: "validate", purpose: "Verificar post-refactor" },
285
+ ],
286
+ knowledge_refs: [],
287
+ tips: ["Extraer sub-componentes si se excede el límite de líneas"],
288
+ },
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Detecta el tipo de tarea según las keywords en la descripción.
294
+ *
295
+ * @param {string} description - Descripción de la tarea
296
+ * @returns {string} ID del tipo de tarea
297
+ */
298
+ function detectTaskType(description) {
299
+ const lower = description.toLowerCase()
300
+ let bestType = "unknown"
301
+ let bestScore = 0
302
+
303
+ for (const [type, patterns] of Object.entries(TYPE_PATTERNS)) {
304
+ let score = 0
305
+ for (const pattern of patterns) {
306
+ if (lower.includes(pattern)) {
307
+ // Más peso para matches al inicio
308
+ score += lower.startsWith(pattern) ? 2 : 1
309
+ }
310
+ }
311
+ if (score > bestScore) {
312
+ bestScore = score
313
+ bestType = type
314
+ }
315
+ }
316
+
317
+ return bestType
318
+ }
319
+
320
+ /**
321
+ * Calcula el puntaje de confianza para un workflow contra la descripción.
322
+ *
323
+ * @param {Workflow} workflow - Workflow a evaluar
324
+ * @param {string} description - Descripción de la tarea en minúsculas
325
+ * @returns {number} Puntaje 0-1
326
+ */
327
+ function scoreWorkflow(workflow, description) {
328
+ const triggers = workflow.trigger || []
329
+ if (triggers.length === 0) return 0
330
+
331
+ let totalScore = 0
332
+ const maxPossible = triggers.length * 2
333
+
334
+ for (const trigger of triggers) {
335
+ const lowerTrigger = trigger.toLowerCase()
336
+ if (description.includes(lowerTrigger)) {
337
+ // Match exacto pesa más
338
+ totalScore += 2
339
+ } else {
340
+ // Match parcial (palabras contenidas)
341
+ const words = lowerTrigger.split(/\s+/)
342
+ const matchCount = words.filter((w) => w.length > 2 && description.includes(w)).length
343
+ totalScore += matchCount / Math.max(words.length, 1)
344
+ }
345
+ }
346
+
347
+ // Bonus por tipo de tarea alineado
348
+ for (const [, patterns] of Object.entries(TYPE_PATTERNS)) {
349
+ for (const pattern of patterns) {
350
+ if (description.includes(pattern)) {
351
+ totalScore += 1
352
+ break
353
+ }
354
+ }
355
+ }
356
+
357
+ return Math.min(totalScore / Math.max(maxPossible, 1), 1)
358
+ }
359
+
360
+ /**
361
+ * Encuentra planes de trabajo existentes que puedan necesitar ajustes
362
+ * por la nueva tarea.
363
+ *
364
+ * @param {string} description - Descripción de la nueva tarea
365
+ * @returns {Array} Planes existentes que podrían verse afectados
366
+ */
367
+ function findPlanConflicts(description) {
368
+ const plansDir = join(process.cwd(), ".opencode", "work-context", "PLANS")
369
+ if (!existsSync(plansDir)) return []
370
+
371
+ let files = []
372
+ try {
373
+ files = readdirSync(plansDir).filter((f) => f.endsWith(".md"))
374
+ } catch {
375
+ return []
376
+ }
377
+
378
+ if (files.length === 0) return []
379
+
380
+ const conflicts = []
381
+ const lowerDesc = description.toLowerCase()
382
+ const words = lowerDesc.split(/\s+/).filter((w) => w.length > 4)
383
+
384
+ if (words.length === 0) return []
385
+
386
+ for (const file of files) {
387
+ try {
388
+ const content = readFileSync(join(plansDir, file), "utf-8")
389
+ const lower = content.toLowerCase()
390
+
391
+ const overlapCount = words.filter((w) => lower.includes(w)).length
392
+
393
+ if (overlapCount >= 2) {
394
+ conflicts.push({
395
+ file,
396
+ overlapCount,
397
+ existingTask: extractPlanTitle(content),
398
+ })
399
+ }
400
+ } catch {
401
+ // Saltar archivos que no se puedan leer
402
+ }
403
+ }
404
+
405
+ return conflicts.sort((a, b) => b.overlapCount - a.overlapCount)
406
+ }
407
+
408
+ /**
409
+ * Extrae el título de un plan markdown.
410
+ *
411
+ * @param {string} content
412
+ * @returns {string}
413
+ */
414
+ function extractPlanTitle(content) {
415
+ const match = content.match(/^#\s+(.+)/m)
416
+ return match ? match[1].trim() : "Plan sin título"
417
+ }
418
+
419
+ /**
420
+ * Busca si hay tickets abiertos que ya cubren parte de la tarea.
421
+ *
422
+ * @param {string} description
423
+ * @returns {Array} Tickets existentes que se solapan
424
+ */
425
+ function findExistingTickets(description) {
426
+ const bugsDir = join(process.cwd(), ".opencode", "bugs")
427
+ if (!existsSync(bugsDir)) return []
428
+
429
+ try {
430
+ const files = readdirSync(bugsDir).filter((f) => f.endsWith(".md"))
431
+ if (files.length === 0) return []
432
+
433
+ const lowerDesc = description.toLowerCase()
434
+ const words = lowerDesc.split(/\s+/).filter((w) => w.length > 4)
435
+ if (words.length === 0) return []
436
+
437
+ const overlapping = []
438
+
439
+ for (const file of files) {
440
+ try {
441
+ const content = readFileSync(join(bugsDir, file), "utf-8")
442
+ const statusMatch = content.match(/\*\*Estado\*\*\s+\|\s+(\w+)/)
443
+ if (statusMatch && ["open", "wip"].includes(statusMatch[1])) {
444
+ const lower = content.toLowerCase()
445
+ const overlapCount = words.filter((w) => lower.includes(w)).length
446
+ if (overlapCount >= 2) {
447
+ overlapping.push({
448
+ file,
449
+ id: file.split("-")[0],
450
+ overlapCount,
451
+ })
452
+ }
453
+ }
454
+ } catch {
455
+ // Saltar archivos que no se puedan leer
456
+ }
457
+ }
458
+
459
+ return overlapping.sort((a, b) => b.overlapCount - a.overlapCount)
460
+ } catch {
461
+ return []
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Renumera los pasos secuencialmente.
467
+ *
468
+ * @param {Array} steps
469
+ * @returns {Array}
470
+ */
471
+ function renumberSteps(steps) {
472
+ return steps.map((s, i) => ({ ...s, step: i + 1 }))
473
+ }
474
+
475
+ /**
476
+ * Selecciona el workflow más adecuado para una descripción de tarea.
477
+ *
478
+ * @param {string} taskDescription - Descripción de la tarea a realizar
479
+ * @param {object} [options] - Opciones adicionales
480
+ * @param {boolean} [options.checkConflicts=true] - Buscar conflictos con planes existentes
481
+ * @returns {WorkflowMatch} Resultado del matching
482
+ */
483
+ export function selectWorkflow(taskDescription, options = {}) {
484
+ const { checkConflicts = true } = options
485
+ const desc = taskDescription.trim()
486
+
487
+ if (!desc) {
488
+ return {
489
+ workflowId: "unknown",
490
+ label: "Workflow Genérico",
491
+ icon: "❓",
492
+ steps: [
493
+ { step: 1, action: "analyze_project", purpose: "Analizar proyecto" },
494
+ { step: 2, action: "work_context_plan", purpose: "Planificar" },
495
+ { step: 3, action: "implement", purpose: "Implementar" },
496
+ { step: 4, action: "validate", purpose: "Verificar" },
497
+ ],
498
+ confidence: 0,
499
+ taskType: "unknown",
500
+ requiresTicket: true,
501
+ requiresDocs: true,
502
+ requiresPlan: true,
503
+ pendingAdjustments: [],
504
+ existingTickets: [],
505
+ }
506
+ }
507
+
508
+ const workflows = loadWorkflows()
509
+ const lowerDesc = desc.toLowerCase()
510
+ const taskType = detectTaskType(desc)
511
+
512
+ // Puntuar cada workflow
513
+ const scored = Object.entries(workflows).map(([id, wf]) => ({
514
+ workflowId: id,
515
+ label: wf.label,
516
+ icon: wf.icon || "📋",
517
+ steps: wf.steps,
518
+ knowledge_refs: wf.knowledge_refs || [],
519
+ tips: wf.tips || [],
520
+ score: scoreWorkflow(wf, lowerDesc),
521
+ }))
522
+
523
+ // Ordenar por score descendente
524
+ scored.sort((a, b) => b.score - a.score)
525
+
526
+ // Seleccionar el mejor
527
+ const best = scored[0]
528
+ const confidence = best.score
529
+
530
+ let selected
531
+ if (confidence < 0.1) {
532
+ // Baja confianza: buscar por tipo de tarea
533
+ const typeWorkflowId = Object.keys(workflows).find(
534
+ (id) => id === taskType || (workflows[id].label || "").toLowerCase().includes(taskType)
535
+ )
536
+ if (typeWorkflowId && workflows[typeWorkflowId]) {
537
+ const wf = workflows[typeWorkflowId]
538
+ selected = {
539
+ workflowId: typeWorkflowId,
540
+ label: wf.label,
541
+ icon: wf.icon || "📋",
542
+ steps: wf.steps,
543
+ knowledge_refs: wf.knowledge_refs || [],
544
+ tips: wf.tips || [],
545
+ }
546
+ } else {
547
+ selected = {
548
+ workflowId: "generic",
549
+ label: "Workflow Genérico",
550
+ icon: "📋",
551
+ steps: [
552
+ { step: 1, action: "analyze_project", purpose: "Analizar proyecto" },
553
+ { step: 2, action: "context_unified", query: desc, purpose: "Buscar contexto" },
554
+ { step: 3, action: "work_context_plan", purpose: "Planificar implementación" },
555
+ { step: 4, action: "implement", purpose: "Implementar" },
556
+ { step: 5, action: "validate", purpose: "Verificar" },
557
+ { step: 6, action: "docs_updated", purpose: "Actualizar documentación" },
558
+ ],
559
+ knowledge_refs: [],
560
+ tips: ["Siempre validar con validate antes de cerrar"],
561
+ }
562
+ }
563
+ } else {
564
+ selected = best
565
+ }
566
+
567
+ // Obtener requerimientos según tipo de tarea
568
+ const typeInfo = TASK_TYPES[taskType] || TASK_TYPES.unknown
569
+
570
+ // Preparar pasos con inyecciones de contexto
571
+ const steps = [...selected.steps]
572
+
573
+ // Buscar conflictos con planes existentes
574
+ const pendingAdjustments = checkConflicts ? findPlanConflicts(desc) : []
575
+
576
+ // Buscar tickets existentes solapados
577
+ const existingTickets = findExistingTickets(desc)
578
+
579
+ // Inyectar pasos contextuales (en orden inverso para no romper índices)
580
+ if (checkConflicts && pendingAdjustments.length > 0) {
581
+ steps.unshift({
582
+ step: 0,
583
+ action: "adjust_existing_plans",
584
+ purpose: `Ajustar ${pendingAdjustments.length} plan(es) existente(s) que se solapan con esta tarea`,
585
+ plans: pendingAdjustments,
586
+ })
587
+ }
588
+
589
+ if (existingTickets.length > 0) {
590
+ steps.unshift({
591
+ step: 0,
592
+ action: "review_existing_tickets",
593
+ purpose: `Revisar ${existingTickets.length} ticket(s) existente(s) que cubren parte de esta tarea`,
594
+ tickets: existingTickets,
595
+ })
596
+ }
597
+
598
+ // Siempre inyectar paso de ticket si es requerido
599
+ if (typeInfo.requiresTicket) {
600
+ const hasTicketStep = steps.some(
601
+ (s) => s.action === "create_ticket" || s.action === "ticket_create"
602
+ )
603
+ if (!hasTicketStep) {
604
+ steps.unshift({
605
+ step: 0,
606
+ action: "create_ticket",
607
+ purpose: "Crear ticket para trackear esta tarea (requerido antes de implementar)",
608
+ })
609
+ }
610
+ }
611
+
612
+ return {
613
+ workflowId: selected.workflowId,
614
+ label: selected.label,
615
+ icon: selected.icon,
616
+ steps: renumberSteps(steps),
617
+ confidence,
618
+ knowledge_refs: selected.knowledge_refs,
619
+ tips: selected.tips,
620
+ taskType,
621
+ requiresTicket: typeInfo.requiresTicket,
622
+ requiresDocs: typeInfo.requiresDocs,
623
+ requiresPlan: typeInfo.requiresPlan,
624
+ pendingAdjustments,
625
+ existingTickets,
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Lista todos los workflows disponibles.
631
+ *
632
+ * @returns {Array<{id:string, label:string, icon:string, triggerCount:number, stepCount:number}>}
633
+ */
634
+ export function listWorkflows() {
635
+ const workflows = loadWorkflows()
636
+ return Object.entries(workflows).map(([id, wf]) => ({
637
+ id,
638
+ label: wf.label,
639
+ icon: wf.icon || "📋",
640
+ triggerCount: (wf.trigger || []).length,
641
+ stepCount: (wf.steps || []).length,
642
+ }))
643
+ }
644
+
645
+ /**
646
+ * Genera un resumen legible del workflow seleccionado para mostrar en consola.
647
+ *
648
+ * @param {WorkflowMatch} match - Resultado de selectWorkflow()
649
+ * @returns {string} Texto formateado
650
+ */
651
+ export function formatWorkflowSummary(match) {
652
+ const lines = []
653
+ lines.push(`\n${match.icon} Workflow: ${match.label}`)
654
+ lines.push(` Tipo: ${match.taskType}`)
655
+ lines.push(` Confianza: ${(match.confidence * 100).toFixed(0)}%`)
656
+ lines.push("")
657
+
658
+ if (match.pendingAdjustments && match.pendingAdjustments.length > 0) {
659
+ lines.push(" ⚠️ Planes existentes que requieren ajuste:")
660
+ for (const p of match.pendingAdjustments) {
661
+ lines.push(` • ${p.file}: "${p.existingTask}"`)
662
+ }
663
+ lines.push("")
664
+ }
665
+
666
+ if (match.existingTickets && match.existingTickets.length > 0) {
667
+ lines.push(" 📌 Tickets existentes que se solapan:")
668
+ for (const t of match.existingTickets) {
669
+ lines.push(` • ${t.id} (${t.file})`)
670
+ }
671
+ lines.push("")
672
+ }
673
+
674
+ lines.push(" Pasos:")
675
+ for (const step of match.steps) {
676
+ const desc = step.purpose ? `— ${step.purpose}` : ""
677
+ const ticketInfo = step.tickets ? ` (${step.tickets.length} tickets)` : ""
678
+ const planInfo = step.plans ? ` (${step.plans.length} planes)` : ""
679
+ lines.push(` ${step.step}. ${step.action} ${desc}${ticketInfo}${planInfo}`)
680
+ }
681
+
682
+ if (match.knowledge_refs && match.knowledge_refs.length > 0) {
683
+ lines.push("")
684
+ lines.push(" 📚 Referencias de conocimiento:")
685
+ for (const ref of match.knowledge_refs) {
686
+ lines.push(` • ${ref}`)
687
+ }
688
+ }
689
+
690
+ if (match.tips && match.tips.length > 0) {
691
+ lines.push("")
692
+ lines.push(" 💡 Tips:")
693
+ for (const tip of match.tips) {
694
+ lines.push(` • ${tip}`)
695
+ }
696
+ }
697
+
698
+ lines.push("")
699
+ const ticketStatus = match.requiresTicket ? "✅ Requiere ticket" : "⏭️ No requiere ticket"
700
+ const docsStatus = match.requiresDocs ? "✅ Requiere docs" : "⏭️ No requiere docs"
701
+ const planStatus = match.requiresPlan ? "✅ Requiere plan" : "⏭️ No requiere plan"
702
+ lines.push(` ${ticketStatus}`)
703
+ lines.push(` ${docsStatus}`)
704
+ lines.push(` ${planStatus}`)
705
+
706
+ return lines.join("\n")
707
+ }