openprompt-lang 1.2.7 → 1.4.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.
Files changed (79) hide show
  1. package/README.md +62 -8
  2. package/bin/cli.js +2 -0
  3. package/docs/00-ARCHITECTURE/OPL-BOOST-MULTI-AGENT.md +406 -0
  4. package/docs/02-STANDARDS/AGENTS.template.md +89 -0
  5. package/docs/02-STANDARDS/ticket-driven-development.md +99 -0
  6. package/docs/04-TICKETS/BOOST-001-profile-registry.md +66 -0
  7. package/docs/04-TICKETS/BOOST-002-context-compression.md +58 -0
  8. package/docs/04-TICKETS/BOOST-003-template-hydration.md +69 -0
  9. package/docs/04-TICKETS/BOOST-004-fewshot-engine.md +58 -0
  10. package/docs/04-TICKETS/BOOST-005-agent-pool.md +69 -0
  11. package/docs/04-TICKETS/BOOST-006-specialized-agents.md +53 -0
  12. package/docs/04-TICKETS/BOOST-007-validation-loop.md +56 -0
  13. package/docs/04-TICKETS/BOOST-008-orchestrator.md +71 -0
  14. package/docs/04-TICKETS/BOOST-009-cache-system.md +56 -0
  15. package/docs/04-TICKETS/BOOST-010-cli-mcp.md +67 -0
  16. package/docs/04-TICKETS/BOOST-011-self-learning.md +50 -0
  17. package/docs/04-TICKETS/BOOST-012-prompt-preamble.md +109 -0
  18. package/docs/04-TICKETS/BOOST-013-hydrator-duplicate-code.md +132 -0
  19. package/docs/04-TICKETS/BOOST-014-multiagent-missing-parts.md +87 -0
  20. package/docs/04-TICKETS/BOOST-015-skeleton-type-missing.md +76 -0
  21. package/docs/04-TICKETS/BOOST-016-output-path-duplicate.md +68 -0
  22. package/docs/04-TICKETS/INDEX.md +89 -0
  23. package/docs/04-TICKETS/_archive/BOOST-005-micro-tasking.md +67 -0
  24. package/docs/04-TICKETS/_archive/BOOST-006-validation-loop.md +66 -0
  25. package/docs/04-TICKETS/_archive/BOOST-007-progressive-pipeline.md +69 -0
  26. package/docs/04-TICKETS/_archive/BOOST-008-cli-mcp-integration.md +74 -0
  27. package/docs/AI_CONTEXT.md +16 -0
  28. package/docs/EMBEDDINGS.md +214 -0
  29. package/docs/ONBOARDING_WORKFLOW.md +151 -0
  30. package/docs/OPL_ACADEMIC_ISSUES.md +158 -0
  31. package/docs/WEB_SCRAPER_PLAN.md +454 -0
  32. package/package.json +9 -2
  33. package/scripts/postinstall.js +37 -0
  34. package/src/boost/agent-pool.js +442 -0
  35. package/src/boost/agents/index.js +79 -0
  36. package/src/boost/cache.js +241 -0
  37. package/src/boost/context-compressor.js +354 -0
  38. package/src/boost/fewshot-retriever.js +332 -0
  39. package/src/boost/hardware-detector.js +486 -0
  40. package/src/boost/hydrator.js +398 -0
  41. package/src/boost/index.js +60 -0
  42. package/src/boost/orchestrator.js +615 -0
  43. package/src/boost/preamble.js +217 -0
  44. package/src/boost/profile-registry.js +264 -0
  45. package/src/boost/self-learn.js +247 -0
  46. package/src/boost/skeletons/component.skeleton.js +24 -0
  47. package/src/boost/skeletons/hook.skeleton.js +27 -0
  48. package/src/boost/skeletons/index.js +67 -0
  49. package/src/boost/skeletons/page.skeleton.js +22 -0
  50. package/src/boost/skeletons/service.skeleton.js +20 -0
  51. package/src/boost/skeletons/store.skeleton.js +18 -0
  52. package/src/boost/skeletons/type.skeleton.js +11 -0
  53. package/src/boost/task-dispatcher.js +142 -0
  54. package/src/boost/validation-loop.js +495 -0
  55. package/src/cli/commands-boost.js +394 -0
  56. package/src/cli/commands-knowledge.js +1 -0
  57. package/src/cli/commands-opl.js +79 -1
  58. package/src/cli/commands-workflow.js +125 -6
  59. package/src/commands/init-core.js +169 -5
  60. package/src/commands/knowledge-ops.js +52 -0
  61. package/src/commands/opl-embeddings.js +556 -0
  62. package/src/commands/opl-help.js +26 -2
  63. package/src/commands/opl-search.js +106 -2
  64. package/src/commands/opl-webscrape.js +390 -0
  65. package/src/commands/workflow/epic-cli.js +192 -0
  66. package/src/commands/workflow/select.js +146 -0
  67. package/src/commands/workflow/sprint-cli.js +174 -0
  68. package/src/core/webscrape/analyzer.js +481 -0
  69. package/src/core/webscrape/deep-scraper.js +1027 -0
  70. package/src/core/workflow/epic-manager.js +845 -0
  71. package/src/core/workflow/gates.js +180 -1
  72. package/src/core/workflow/selector.js +707 -0
  73. package/src/embeddings/chunker.js +450 -0
  74. package/src/embeddings/embedder.js +431 -0
  75. package/src/embeddings/index-pipeline.js +320 -0
  76. package/src/embeddings/vector-store.js +505 -0
  77. package/src/mcp-refactor/handlers/boost.js +295 -0
  78. package/src/mcp-refactor/router.js +19 -0
  79. package/src/mcp-refactor/tools.js +113 -0
@@ -0,0 +1,615 @@
1
+ // @use(kind, contract, limit, error)
2
+ // @kind(util)
3
+ // @contract(in: task, options -> out: boost -> { code, metadata, report } @error: BoostPipelineError)
4
+ // @limit(lines: 350)
5
+
6
+ /**
7
+ * Boost Orchestrator — Pipeline de generación multi-etapa.
8
+ *
9
+ * Orquesta todos los componentes Boost en un flujo coherente.
10
+ * Soporta dos modos:
11
+ *
12
+ * **Single-pass** (default): compress → few-shot → hydrate → quick-validate
13
+ * Usa el Agent Pool solo para la generación central.
14
+ *
15
+ * **Multi-agent** (opt-in): compress → dispatch DAG → agent pool → assemble → validate
16
+ * Usa agentes especializados para cada parte del código.
17
+ *
18
+ * Uso:
19
+ * import { boost } from './boost/orchestrator.js'
20
+ * const result = await boost('crea hook useAuth con Supabase', {
21
+ * kind: 'hook',
22
+ * profile: 'medium',
23
+ * })
24
+ * // → { code, metadata: { duration, stages: [...], profile }, report }
25
+ */
26
+
27
+ import { getProfile, isBoostEnabled } from "./profile-registry.js"
28
+ import { compressContext, compressAgentsMd } from "./context-compressor.js"
29
+ import { retrieveExamples, buildFewShotPrompt } from "./fewshot-retriever.js"
30
+ import { hydrate, hydrateFromAgentResults } from "./hydrator.js"
31
+ import { quickValidate, validateCode, feedbackLoop } from "./validation-loop.js"
32
+ import { plan, detectKind } from "./task-dispatcher.js"
33
+ import { agentPool } from "./agent-pool.js"
34
+
35
+ // ──────────────────────────────────────────────
36
+ // Configuración
37
+ // ──────────────────────────────────────────────
38
+
39
+ const DEFAULT_OPTIONS = {
40
+ profile: null, // auto-detect
41
+ kind: null, // auto-detect
42
+ dryRun: false, // solo mostrar plan
43
+ output: null, // archivo de salida (opcional)
44
+ validate: true, // ejecutar validation loop
45
+ multiAgent: false, // usar multi-agente (opt-in)
46
+ contextMode: "safe", // modo de compresión: safe | aggressive | disabled
47
+ maxExamples: 3, // máx ejemplos few-shot
48
+ }
49
+
50
+ // ──────────────────────────────────────────────
51
+ // Pipeline stages
52
+ // ──────────────────────────────────────────────
53
+
54
+ /**
55
+ * Stage 0: Detectar perfil
56
+ */
57
+ function stageProfile(options) {
58
+ const startTime = Date.now()
59
+ const profile = options.profile || getProfile()
60
+ return {
61
+ name: "profile-detect",
62
+ duration: Date.now() - startTime,
63
+ result: profile ? { name: profile.name, mode: profile.agentPool?.mode || "queue" } : null,
64
+ profile,
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Stage 1: Comprimir contexto
70
+ */
71
+ function stageCompress(task, kind, profile, options) {
72
+ const startTime = Date.now()
73
+
74
+ // Comprimir AGENTS.md
75
+ const compressed = compressAgentsMd(task, profile, {
76
+ mode: options.contextMode || "safe",
77
+ kind,
78
+ })
79
+
80
+ return {
81
+ name: "context-compress",
82
+ duration: Date.now() - startTime,
83
+ result: compressed
84
+ ? {
85
+ originalChars: compressed.metrics.originalChars,
86
+ compressedChars: compressed.metrics.compressedChars,
87
+ reductionPercent: compressed.metrics.reductionPercent,
88
+ sectionsKept: compressed.metrics.sectionsKept,
89
+ }
90
+ : null,
91
+ compressed,
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Stage 2: Buscar ejemplos few-shot
97
+ */
98
+ function stageFewShot(task, kind, options) {
99
+ const startTime = Date.now()
100
+
101
+ const examples = retrieveExamples(kind, task, {
102
+ maxExamples: options.maxExamples || 3,
103
+ })
104
+
105
+ return {
106
+ name: "fewshot-retrieve",
107
+ duration: Date.now() - startTime,
108
+ result: {
109
+ examplesFound: examples.length,
110
+ sources: examples.map((e) => e.file),
111
+ },
112
+ examples,
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Stage 3: DAG planning (solo multi-agent)
118
+ */
119
+ function stagePlan(task, kind) {
120
+ const startTime = Date.now()
121
+ const dag = plan(task, kind)
122
+
123
+ return {
124
+ name: "task-plan",
125
+ duration: Date.now() - startTime,
126
+ result: {
127
+ kind: dag.kind,
128
+ nodes: dag.nodeCount,
129
+ parallel: dag.stages.parallel.length,
130
+ sequential: dag.stages.sequential.length,
131
+ },
132
+ dag,
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Stage 4: Generar código con Agent Pool
138
+ */
139
+ async function stageGenerate(task, kind, compressed, examples, dag, profile, options) {
140
+ const startTime = Date.now()
141
+
142
+ if (options.multiAgent) {
143
+ // MODO MULTI-AGENTE: ejecutar DAG con agentes especializados
144
+ return await stageGenerateMultiAgent(task, kind, compressed, examples, dag, profile, options)
145
+ }
146
+
147
+ // MODO SINGLE-PASS: generar con un solo agente (logic)
148
+ // El agente recibe contexto comprimido + ejemplos + instrucciones
149
+ const context = compressed?.context || ""
150
+ const fewShotPrompt = examples.length > 0
151
+ ? buildFewShotPrompt(examples, `Genera un ${kind} para: ${task}`)
152
+ : `Genera un ${kind} para: ${task}`
153
+
154
+ const fullPrompt = [
155
+ context ? `## Contexto del Proyecto\n\n${context.substring(0, 2000)}\n\n` : "",
156
+ fewShotPrompt,
157
+ ].join("")
158
+
159
+ const result = await agentPool.submit("logic", fullPrompt, profile)
160
+ const code = result.text
161
+
162
+ return {
163
+ name: "generate",
164
+ duration: Date.now() - startTime,
165
+ result: {
166
+ mode: "single-pass",
167
+ tokensPerSecond: result.metrics?.tokensPerSecond,
168
+ tokenCount: result.metrics?.tokenCount,
169
+ codeLength: code.length,
170
+ },
171
+ code,
172
+ agentMetrics: result.metrics,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Stage 4b: Generación multi-agente (DAG)
178
+ */
179
+ async function stageGenerateMultiAgent(task, kind, compressed, examples, dag, profile, options) {
180
+ const startTime = Date.now()
181
+ const context = compressed?.context || ""
182
+ const results = {}
183
+
184
+ // Fase paralela: nodos sin dependencias
185
+ const parallelNodes = dag.nodes.filter((n) => n.deps.length === 0)
186
+ const parallelResults = await Promise.all(
187
+ parallelNodes.map(async (node) => {
188
+ const agentInput = [
189
+ task,
190
+ context ? `\nContexto:\n${context.substring(0, 1000)}` : "",
191
+ examples.length > 0 ? `\nEjemplos:\n${examples.map((e) => e.file).join(", ")}` : "",
192
+ ].join("\n")
193
+
194
+ const result = await agentPool.submit(node.agent, agentInput, profile)
195
+ return { nodeId: node.id, ...result }
196
+ })
197
+ )
198
+
199
+ for (const r of parallelResults) {
200
+ results[r.nodeId] = r.text
201
+ }
202
+
203
+ // Fase secuencial: nodos con dependencias
204
+ const sequentialNodes = dag.nodes.filter((n) => n.deps.length > 0)
205
+ for (const node of sequentialNodes) {
206
+ // Armar input con resultados de dependencias
207
+ const depsOutput = node.deps.map((depId) => {
208
+ const depNode = dag.nodes.find((n) => n.id === depId)
209
+ return `### ${depNode?.description || depId}\n${results[depId] || ""}`
210
+ }).join("\n\n")
211
+
212
+ const agentInput = [
213
+ `Task: ${task}`,
214
+ `Kind: ${kind}`,
215
+ "",
216
+ "Outputs from previous agents:",
217
+ depsOutput,
218
+ "",
219
+ `Your role: ${node.description}`,
220
+ ].join("\n")
221
+
222
+ const result = await agentPool.submit(node.agent, agentInput, profile)
223
+ results[node.id] = result.text
224
+ }
225
+
226
+ // Ensamblar resultados en código final
227
+ // Extraer nombre de la tarea para el hydrator
228
+ const taskName = extractNameFromTask(task)
229
+ const assembledCode = assembleFromDAG(kind, results, taskName)
230
+ const hydratorStart = Date.now()
231
+
232
+ // Aplicar la misma lógica de smart detection que stageHydrate
233
+ const hasImports = /^import\s/m.test(assembledCode)
234
+ const hasExport = /^export\s/m.test(assembledCode)
235
+
236
+ let finalCode
237
+ let hydrationResult
238
+
239
+ if (hasImports && hasExport) {
240
+ // Código completo del LLM — solo inyectar anotaciones
241
+ const annotations = generateOPLAnnotations(kind, assembledCode)
242
+ finalCode = annotations + "\n\n" + assembledCode.trim()
243
+ hydrationResult = { hydrated: false, reason: "multi-agent-complete-injection" }
244
+ } else {
245
+ // Código parcial — hidratar con skeleton
246
+ const hydrated = hydrateFromAgentResults(kind, { ...results, name: taskName })
247
+ finalCode = hydrated.code || assembledCode
248
+ hydrationResult = { hydrated: true, warnings: hydrated.warnings.length }
249
+ }
250
+
251
+ return {
252
+ name: "generate",
253
+ duration: Date.now() - startTime,
254
+ result: {
255
+ mode: "multi-agent",
256
+ parallelNodes: parallelNodes.length,
257
+ sequentialNodes: sequentialNodes.length,
258
+ agentResults: Object.keys(results).length,
259
+ },
260
+ code: finalCode,
261
+ agentResults: results,
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Ensamblado básico de resultados multi-agente.
267
+ */
268
+ function assembleFromDAG(kind, results, taskName) {
269
+ const parts = []
270
+ if (results.types) parts.push(results.types)
271
+ if (results.state) parts.push(results.state)
272
+ if (results.logic) parts.push(results.logic)
273
+ return parts.join("\n\n")
274
+ }
275
+
276
+ /**
277
+ * Stage 5: Hidratar código final
278
+ *
279
+ * Estrategia de hidratación:
280
+ * 1. Si el código ya tiene @kind() → usar directo (ya anotado)
281
+ * 2. Si el código tiene imports + exports → código completo del LLM, solo inyectar anotaciones
282
+ * 3. Si es código parcial → hidratar con skeleton
283
+ */
284
+ function stageHydrate(kind, code, profile) {
285
+ const startTime = Date.now()
286
+
287
+ // Caso 1: Si el código ya tiene anotaciones OPL, usarlo directamente
288
+ if (/@kind\(/.test(code)) {
289
+ return {
290
+ name: "hydrate",
291
+ duration: Date.now() - startTime,
292
+ result: { hydrated: false, reason: "already-annotated" },
293
+ code,
294
+ }
295
+ }
296
+
297
+ // Caso 2: Si el código tiene imports y exports, es código completo del LLM.
298
+ // No envolver en skeleton — solo agregar anotaciones OPL al inicio.
299
+ const hasImports = /^import\s/m.test(code)
300
+ const hasExport = /^export\s/m.test(code)
301
+
302
+ if (hasImports && hasExport) {
303
+ const annotations = generateOPLAnnotations(kind, code)
304
+ const annotatedCode = annotations + "\n\n" + code.trim()
305
+
306
+ return {
307
+ name: "hydrate",
308
+ duration: Date.now() - startTime,
309
+ result: { hydrated: false, reason: "llm-complete-injection" },
310
+ code: annotatedCode,
311
+ }
312
+ }
313
+
314
+ // Caso 3: Código parcial — hidratar con skeleton template
315
+ const hydrated = hydrate(kind, {
316
+ imports: "",
317
+ types: "",
318
+ logic: code,
319
+ name: "Generated",
320
+ props: "",
321
+ description: "Generated by OPL Boost",
322
+ })
323
+
324
+ return {
325
+ name: "hydrate",
326
+ duration: Date.now() - startTime,
327
+ result: {
328
+ hydrated: true,
329
+ warnings: hydrated.warnings.length,
330
+ validationsPassed: hydrated.validations.filter((v) => v.passed).length,
331
+ },
332
+ code: hydrated.code,
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Extrae el nombre de una función/componente de la descripción de la tarea.
338
+ * Ejemplos:
339
+ * "hook useAuth para autenticación" → "useAuth"
340
+ * "service ProductService para ecommerce" → "ProductService"
341
+ * "store useCartStore con zustand" → "useCartStore"
342
+ * "componente ProductCard" → "ProductCard"
343
+ */
344
+ function extractNameFromTask(task) {
345
+ // Patrón: kind name (hook useAuth, service ProductService, etc.)
346
+ const kindNamePattern = /\b(?:hook|service|store|component|page|util)\s+(use\w+|[A-Z]\w*(?:Service|Store|Component|Page|Hook|Manager|Handler))\b/i
347
+ const kindNameMatch = task.match(kindNamePattern)
348
+ if (kindNameMatch) return kindNameMatch[1]
349
+
350
+ // Patrón: useXxx (nombre de hook)
351
+ const hookPattern = /\b(use\w+)\b/i
352
+ const hookMatch = task.match(hookPattern)
353
+ if (hookMatch) return hookMatch[1]
354
+
355
+ // Patrón: XxxService, XxxStore, XxxComponent
356
+ const pascalPattern = /\b([A-Z]\w+(?:Service|Store|Component|Page|Manager|Handler))\b/
357
+ const pascalMatch = task.match(pascalPattern)
358
+ if (pascalMatch) return pascalMatch[1]
359
+
360
+ // Patrón: cualquier palabra PascalCase
361
+ const camelPattern = /\b([A-Z][a-zA-Z]+)\b/
362
+ const camelMatch = task.match(camelPattern)
363
+ if (camelMatch) return camelMatch[1]
364
+
365
+ return "Generated"
366
+ }
367
+
368
+ /**
369
+ * Genera anotaciones OPL para inyectar al inicio de un archivo completo del LLM.
370
+ */
371
+ function generateOPLAnnotations(kind, code) {
372
+ const lineCount = code.split("\n").length
373
+ const kindLimits = { hook: 80, component: 120, page: 200, service: 150, store: 100, util: 100 }
374
+ const limit = kindLimits[kind] || 200
375
+
376
+ let annotation = `// @use(kind, contract, limit)\n// @kind(${kind})`
377
+
378
+ // Detectar contrato básico del código
379
+ const exportMatch = code.match(/export\s+(?:async\s+)?function\s+(\w+)/)
380
+ const funcName = exportMatch ? exportMatch[1] : ""
381
+ if (funcName) {
382
+ annotation += `\n// @contract(in: unknown -> out: ${funcName} result @error: Error)`
383
+ }
384
+
385
+ annotation += `\n// @limit(lines: ${limit})`
386
+
387
+ return annotation
388
+ }
389
+
390
+ /**
391
+ * Stage 6: Validation loop
392
+ */
393
+ async function stageValidate(kind, code, profile, options) {
394
+ if (!options.validate) {
395
+ // Solo quick-validate
396
+ const startTime = Date.now()
397
+ const check = quickValidate(code, kind)
398
+ return {
399
+ name: "quick-validate",
400
+ duration: Date.now() - startTime,
401
+ result: { valid: check.valid, warnings: check.warnings },
402
+ valid: check.valid,
403
+ code,
404
+ }
405
+ }
406
+
407
+ // Feedback loop completo con agente validate
408
+ const startTime = Date.now()
409
+ const result = await feedbackLoop(code, kind, profile, {
410
+ async correctWithAgent(currentCode, errors, feedback) {
411
+ const agentResult = await agentPool.submit("validate", feedback, profile)
412
+ return agentResult.text
413
+ },
414
+ })
415
+
416
+ return {
417
+ name: "validate",
418
+ duration: Date.now() - startTime,
419
+ result: {
420
+ valid: result.valid,
421
+ attempts: result.history.length,
422
+ errorsFound: result.report.errorsFound,
423
+ summary: result.report.summary,
424
+ },
425
+ valid: result.valid,
426
+ code: result.code,
427
+ report: result.report,
428
+ }
429
+ }
430
+
431
+ // ──────────────────────────────────────────────
432
+ // API principal: boost()
433
+ // ──────────────────────────────────────────────
434
+
435
+ /**
436
+ * Orquesta el pipeline completo de generación Boost.
437
+ *
438
+ * @param {string} task - Descripción de la tarea
439
+ * @param {object} [options] - Opciones
440
+ * @param {string} [options.kind] - Tipo de archivo (auto-detecta si se omite)
441
+ * @param {object} [options.profile] - Perfil (auto-detecta si se omite)
442
+ * @param {boolean} [options.dryRun=false] - Solo mostrar plan
443
+ * @param {string} [options.output] - Archivo de salida
444
+ * @param {boolean} [options.validate=true] - Ejecutar validation loop
445
+ * @param {boolean} [options.multiAgent=false] - Usar multi-agente
446
+ * @param {string} [options.contextMode="safe"] - Modo compresión
447
+ * @returns {Promise<{ code: string, metadata: object, report: object }>}
448
+ */
449
+ export async function boost(task, options = {}) {
450
+ const opts = { ...DEFAULT_OPTIONS, ...options }
451
+ const kind = opts.kind || detectKind(task)
452
+ const stages = []
453
+ const startTime = Date.now()
454
+
455
+ // Verificar que Boost esté habilitado
456
+ if (!isBoostEnabled()) {
457
+ return {
458
+ code: "",
459
+ metadata: { error: "Boost deshabilitado en prompt-lang.json" },
460
+ report: { summary: "Boost deshabilitado. Actívalo con boost.enabled = true" },
461
+ }
462
+ }
463
+
464
+ // Stage 0: Profile
465
+ const profileStage = stageProfile(opts)
466
+ stages.push(profileStage)
467
+ const profile = profileStage.profile
468
+
469
+ if (opts.dryRun) {
470
+ return dryRun(task, kind, profile, opts)
471
+ }
472
+
473
+ // Stage 1: Compress
474
+ const compressStage = stageCompress(task, kind, profile, opts)
475
+ stages.push(compressStage)
476
+
477
+ // Stage 2: Few-shot
478
+ const fewShotStage = stageFewShot(task, kind, opts)
479
+ stages.push(fewShotStage)
480
+
481
+ // Stage 3: Plan
482
+ const planStage = stagePlan(task, kind)
483
+ stages.push(planStage)
484
+
485
+ // Stage 4: Generate
486
+ const generateStage = await stageGenerate(
487
+ task, kind,
488
+ compressStage.compressed,
489
+ fewShotStage.examples,
490
+ planStage.dag,
491
+ profile, opts
492
+ )
493
+ stages.push(generateStage)
494
+
495
+ // Stage 5: Hydrate
496
+ const hydrateStage = stageHydrate(kind, generateStage.code, profile)
497
+ stages.push(hydrateStage)
498
+
499
+ // Stage 6: Validate
500
+ const validateStage = await stageValidate(kind, hydrateStage.code, profile, opts)
501
+ stages.push(validateStage)
502
+
503
+ // Calcular metadata
504
+ const totalTime = Date.now() - startTime
505
+ const code = validateStage.code
506
+
507
+ // Guardar a archivo si se especificó
508
+ if (opts.output && code) {
509
+ try {
510
+ const { writeFileSync, mkdirSync, existsSync } = await import("fs")
511
+ const { dirname, isAbsolute, join: joinPath } = await import("path")
512
+ // Si la ruta es absoluta, usarla directamente; si es relativa, resolverla desde cwd
513
+ const outputPath = isAbsolute(opts.output) ? opts.output : joinPath(process.cwd(), opts.output)
514
+ const outputDir = dirname(outputPath)
515
+ if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true })
516
+ writeFileSync(outputPath, code, "utf-8")
517
+ } catch (err) {
518
+ // Error guardando archivo — no fatal
519
+ stages.push({
520
+ name: "output-save",
521
+ duration: 0,
522
+ error: err.message,
523
+ })
524
+ }
525
+ }
526
+
527
+ const metadata = {
528
+ task,
529
+ kind,
530
+ profile: profile?.name || "unknown",
531
+ totalTime,
532
+ stages: stages.map((s) => ({
533
+ name: s.name,
534
+ duration: s.duration,
535
+ result: s.result || null,
536
+ error: s.error || null,
537
+ })),
538
+ multiAgent: opts.multiAgent,
539
+ contextMode: opts.contextMode,
540
+ }
541
+
542
+ const report = {
543
+ summary: validateStage.valid
544
+ ? `✅ Código generado exitosamente en ${(totalTime / 1000).toFixed(1)}s`
545
+ : `⚠️ Código generado con advertencias en ${(totalTime / 1000).toFixed(1)}s`,
546
+ profile: profile?.name || "unknown",
547
+ totalTime,
548
+ stagesCompleted: stages.filter((s) => !s.error).length,
549
+ stagesFailed: stages.filter((s) => s.error).length,
550
+ }
551
+
552
+ return { code, metadata, report }
553
+ }
554
+
555
+ // ──────────────────────────────────────────────
556
+ // Dry-run
557
+ // ──────────────────────────────────────────────
558
+
559
+ function dryRun(task, kind, profile, options) {
560
+ const dag = plan(task, kind)
561
+
562
+ return {
563
+ code: "",
564
+ metadata: {
565
+ task,
566
+ kind,
567
+ profile: profile?.name || "unknown",
568
+ dryRun: true,
569
+ stages: [
570
+ { name: "profile-detect", planned: true, result: { profile: profile?.name } },
571
+ { name: "context-compress", planned: true, result: { mode: options.contextMode, level: profile?.compressionLevel } },
572
+ { name: "fewshot-retrieve", planned: true, result: { maxExamples: options.maxExamples } },
573
+ { name: "task-plan", planned: true, result: { kind, nodes: dag.nodeCount } },
574
+ { name: "generate", planned: true, result: { mode: options.multiAgent ? "multi-agent" : "single-pass" } },
575
+ { name: "hydrate", planned: true, result: {} },
576
+ { name: "validate", planned: true, result: { enabled: options.validate } },
577
+ ],
578
+ },
579
+ plan: {
580
+ task,
581
+ kind,
582
+ profile: profile?.name,
583
+ dag,
584
+ estimatedDuration: estimateDuration(profile, options),
585
+ command: `opl boost generate "${task}"${options.multiAgent ? " --multi-agent" : ""}`,
586
+ },
587
+ report: {
588
+ summary: `📋 Dry-run: generaría un @kind(${kind}) usando perfil ${profile?.name || "unknown"}`,
589
+ duration: "N/A (dry-run)",
590
+ },
591
+ }
592
+ }
593
+
594
+ function estimateDuration(profile, options) {
595
+ const baseTime = profile?.name === "small" ? 30 : profile?.name === "large" ? 8 : 15
596
+ const agentMultiplier = options.multiAgent ? 2.5 : 1
597
+ return `${Math.round(baseTime * agentMultiplier)}s (estimado)`
598
+ }
599
+
600
+ // ──────────────────────────────────────────────
601
+ // Utilidades
602
+ // ──────────────────────────────────────────────
603
+
604
+ /**
605
+ * Código de conveniencia para generar y mostrar resultado.
606
+ * Útil para CLI.
607
+ *
608
+ * @param {string} task
609
+ * @param {object} options
610
+ * @returns {Promise<string>} Código generado
611
+ */
612
+ export async function generateCode(task, options = {}) {
613
+ const result = await boost(task, options)
614
+ return result.code
615
+ }