orquestra-mcp 1.46.1 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.46.1",
3
+ "version": "1.48.0",
4
4
  "description": "Servidor MCP de Orquestra -- 135 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,9 +4,20 @@
4
4
  // analytics/crash reporting ausentes en las 4 apps). Constantes estáticas,
5
5
  // sin acceso a red ni a la base de datos -- mismo criterio que stages.mjs.
6
6
  // get_foundations_checklist las expone tal cual; el agente conectado hace
7
- // la detección real (grep del repo local) y usa report_foundation_gap para
8
- // cada una que confirme ausente -- ver SERVER_INSTRUCTIONS.
7
+ // la detección real (grep del repo local) y reporta con report_foundations
8
+ // (ORQ-400: queda en el checklist de fundamentos del proyecto). Las keys son
9
+ // las mismas que CODE_FOUNDATIONS de orquestra-infra (lib/foundations.mjs);
10
+ // `detectedByServer` = el servidor lo ve solo en el árbol de los repos conectados.
9
11
  export const FOUNDATIONS = [
12
+ {
13
+ key: 'readme',
14
+ label: 'README para levantar el proyecto',
15
+ category: 'calidad',
16
+ why: 'Sin instrucciones de setup (dependencias, variables de entorno, comandos), cada persona nueva -- o tu yo del futuro -- pierde horas reconstruyéndolo.',
17
+ detectionHint: 'README en la raíz del repo que de verdad explique cómo levantarlo, no solo el título.',
18
+ scales: ['landing', 'app', 'platform'],
19
+ detectedByServer: true,
20
+ },
10
21
  {
11
22
  key: 'analytics',
12
23
  label: 'Analytics de producto',
@@ -14,6 +25,7 @@ export const FOUNDATIONS = [
14
25
  why: 'Sin eventos básicos (pantallas, acciones clave) no hay forma de saber qué usa la gente ni de tomar decisiones con datos.',
15
26
  detectionHint: 'Buscar SDKs de analytics en dependencias (Firebase Analytics, Segment, Amplitude, PostHog, Mixpanel) o gtag/GA4 en el HTML -- y confirmar que de verdad se llame (logEvent/track), no solo que esté listada la dependencia.',
16
27
  scales: ['app', 'platform'],
28
+ detectedByServer: false,
17
29
  },
18
30
  {
19
31
  key: 'crash_reporting',
@@ -22,6 +34,7 @@ export const FOUNDATIONS = [
22
34
  why: 'Sin esto, los crashes de usuarios reales son invisibles -- solo se enteran si alguien reporta a mano.',
23
35
  detectionHint: 'Buscar Crashlytics/Sentry/Bugsnag en dependencias, y confirmar inicialización real en el entrypoint de la app (no solo la dependencia listada y sin usar).',
24
36
  scales: ['app', 'platform'],
37
+ detectedByServer: false,
25
38
  },
26
39
  {
27
40
  key: 'error_alerting',
@@ -30,6 +43,7 @@ export const FOUNDATIONS = [
30
43
  why: 'Un log que nadie lee no es monitoreo -- sin una alarma real, un backend caído puede tardar horas o días en notarse.',
31
44
  detectionHint: 'Buscar alarmas de CloudWatch con AlarmActions/SNS, integraciones de Sentry/PagerDuty, o un webhook de errores a Slack/email en la infraestructura.',
32
45
  scales: ['platform'],
46
+ detectedByServer: false,
33
47
  },
34
48
  {
35
49
  key: 'dependency_scanning',
@@ -38,6 +52,7 @@ export const FOUNDATIONS = [
38
52
  why: 'Dependencias con vulnerabilidades conocidas quedan sin parchear indefinidamente si nada avisa cuando aparecen.',
39
53
  detectionHint: 'Buscar .github/dependabot.yml, renovate.json, o un paso de audit (npm audit / pip-audit / similar) en el CI.',
40
54
  scales: ['app', 'platform'],
55
+ detectedByServer: true,
41
56
  },
42
57
  {
43
58
  key: 'ci_pipeline',
@@ -46,5 +61,6 @@ export const FOUNDATIONS = [
46
61
  why: 'Sin lint/tests/build automáticos en cada PR, cada regresión depende de que alguien la note a mano antes de mergear.',
47
62
  detectionHint: 'Buscar workflows de CI (.github/workflows/*.yml, GitLab CI, CircleCI) con jobs que corran en pull_request, no solo en push a main.',
48
63
  scales: ['app', 'platform'],
64
+ detectedByServer: true,
49
65
  },
50
66
  ]
@@ -16,7 +16,7 @@ export const PLATFORM_PLAYBOOKS = {
16
16
  label: 'iOS — App Store',
17
17
  color: '#007AFF',
18
18
  moduleName: 'Publicación en App Store',
19
- moduleDescription: 'Checklist verificable para enviar la app a revisión en App Store Connect, sin sorpresas de último momento.',
19
+ moduleDescription: 'Lo que hay que decidir y preparar para enviar la app a revisión en App Store Connect, sin sorpresas de último momento. Que la ficha esté publicada y que las URLs de privacidad y soporte respondan lo revisa solo el checklist de Buenas prácticas.',
20
20
  moduleTags: ['playbook', 'publicacion'],
21
21
 
22
22
  // Revisado contra App Store Connect real el 19 oct 2026 (ORQ-383). `section`
@@ -190,17 +190,9 @@ export const PLATFORM_PLAYBOOKS = {
190
190
 
191
191
  // `optional: true` = no viene seleccionada por default al aplicar (pasos
192
192
  // de crecimiento que solo tienen sentido con la app ya aprobada).
193
+ // Sin tareas de "verificar URL de privacidad/soporte" (ORQ-398): las
194
+ // prueba solo el checklist de Buenas prácticas y, si falla, ahí se crea la tarea.
193
195
  tasks: [
194
- {
195
- title: 'Verificar/crear política de privacidad pública (URL)',
196
- description: 'App Store Connect pide una URL pública de política de privacidad antes de poder enviar a revisión. Si el proyecto no tiene una, hay que publicarla (puede vivir en la landing).',
197
- type: 'business', priority: 'high', tags: ['publicacion', 'ios'], offsetDays: 2, checkKey: 'privacy',
198
- },
199
- {
200
- title: 'Verificar/crear página o contacto de soporte',
201
- description: 'Apple pide una URL de soporte visible para el usuario (puede ser un mailto, un formulario o una página de FAQ). Probarla en el navegador: tiene que responder 200.',
202
- type: 'business', priority: 'high', tags: ['publicacion', 'ios'], offsetDays: 2, checkKey: 'support',
203
- },
204
196
  {
205
197
  title: 'Confirmar que la app permite eliminar la cuenta desde adentro',
206
198
  description: 'Guideline 5.1.1(v), obligatorio -- "escríbenos por correo" NO cumple el requisito. Tiene que haber un botón real de "Eliminar cuenta" (con confirmación) en Perfil/Ajustes que borre la cuenta y los datos sin salir de la app. Si no existe, constrúyelo ANTES de mandar a revisión, no después de un rechazo.',
@@ -288,7 +280,7 @@ export const PLATFORM_PLAYBOOKS = {
288
280
  label: 'Android — Google Play',
289
281
  color: '#3DDC84',
290
282
  moduleName: 'Publicación en Google Play',
291
- moduleDescription: 'Checklist verificable para publicar la app en Google Play Console, sin sorpresas de último momento.',
283
+ moduleDescription: 'Lo que hay que decidir y preparar para publicar la app en Google Play Console, sin sorpresas de último momento. Que la ficha esté publicada y que la URL de privacidad responda lo revisa solo el checklist de Buenas prácticas.',
292
284
  moduleTags: ['playbook', 'publicacion'],
293
285
 
294
286
  referenceGuide: [
@@ -369,22 +361,9 @@ export const PLATFORM_PLAYBOOKS = {
369
361
  },
370
362
  ],
371
363
 
364
+ // Sin tareas de "verificar URL de privacidad / email de soporte / que la
365
+ // URL responda 200" (ORQ-398): lo prueba solo el checklist de Buenas prácticas.
372
366
  tasks: [
373
- {
374
- title: 'Verificar/crear política de privacidad pública (URL)',
375
- description: 'Play Console (Contenido de la app) pide una URL pública de política de privacidad antes de poder publicar. Si el proyecto no tiene una, hay que publicarla (puede vivir en la landing).',
376
- type: 'business', priority: 'high', tags: ['publicacion', 'android'], offsetDays: 2, checkKey: 'privacy',
377
- },
378
- {
379
- title: 'Verificar/crear página o contacto de soporte',
380
- description: 'Play Console pide un email de contacto de soporte visible en la ficha de Play Store.',
381
- type: 'business', priority: 'high', tags: ['publicacion', 'android'], offsetDays: 2, checkKey: 'support',
382
- },
383
- {
384
- title: 'Probar en el navegador que la URL de privacidad/soporte responde 200 (no solo que "ya se desplegó")',
385
- description: 'Gotcha real: Play Console valida que la URL responda, no solo el formato. Un deploy exitoso no garantiza que rutas como /privacidad carguen -- si el hosting es una SPA (React Router y similares) sin la regla de rewrite configurada (servir index.html para cualquier ruta), da 404 real aunque el build haya salido bien. Probar la URL directo en el navegador, en una pestaña nueva, ANTES de pegarla en el formulario.',
386
- type: 'tech', priority: 'high', tags: ['publicacion', 'android'], offsetDays: 2,
387
- },
388
367
  {
389
368
  title: 'Confirmar pricing y disponibilidad por país',
390
369
  description: 'Definir si la app es gratis o de pago y en qué países se distribuye -- "Disponible en todos los países" es el default razonable salvo restricción regulatoria específica.',
@@ -431,10 +410,15 @@ export const PLATFORM_PLAYBOOKS = {
431
410
  hasForm: false,
432
411
  consoleName: 'SEO y analítica',
433
412
  moduleName: 'SEO y analítica web',
434
- moduleDescription: 'Checklist para que la web/landing nazca medible e indexable: Search Console, analítica, rendimiento y SEO básico, para que no se olvide nada al lanzar.',
413
+ moduleDescription: 'Lo que hay que hacer para que la web/landing nazca medible e indexable: Search Console, analítica y rendimiento. Lo técnico (redirecciones, sitemap, contenido, canonical...) lo revisa solo el checklist de Buenas prácticas.',
435
414
  moduleTags: ['playbook', 'seo', 'analitica'],
436
415
 
437
416
  referenceGuide: [
417
+ {
418
+ key: 'practices_checklist',
419
+ title: 'Lo técnico lo revisa Orquestra solo',
420
+ body: 'Redirecciones 301 (http→https y con/sin www), robots.txt, sitemap, noindex, contenido en el HTML, título y descripción, h1, canonical, og:image y JSON-LD se revisan solos cada lunes en "Buenas prácticas" (arriba en esta página), y te avisa si algo que estaba bien se rompe. No hace falta crear tareas para eso: si un punto falla, ahí mismo puedes crear la tarea.',
421
+ },
438
422
  {
439
423
  key: 'search_console',
440
424
  title: 'Google Search Console',
@@ -485,6 +469,26 @@ export const PLATFORM_PLAYBOOKS = {
485
469
  title: 'HTTPS, dominio canónico y redirecciones',
486
470
  body: 'Elige una sola versión (con o sin www) y redirige 301 las demás, incluido http → https. Evita cadenas de redirecciones y páginas duplicadas bajo dos URLs.',
487
471
  },
472
+ {
473
+ key: 'html_content',
474
+ title: 'El contenido tiene que venir en el HTML',
475
+ body: 'Una SPA (React/Vite) que sirve solo <div id="root"></div> deja a los buscadores casi sin texto: Google lo renderiza tarde y otros buscadores no lo ejecutan. Usa prerender o SSR para que cada ruta pública llegue con su contenido real.',
476
+ },
477
+ {
478
+ key: 'noindex_app',
479
+ title: 'La app y el login no deben competir con la web',
480
+ body: 'Si la app vive en un subdominio (app.tu-dominio.com), ponle <meta name="robots" content="noindex">. Si no, Google puede mostrar la pantalla de login en vez de tu web pública cuando alguien busca tu marca.',
481
+ },
482
+ {
483
+ key: 'own_links',
484
+ title: 'Tus enlaces, siempre a la versión canónica',
485
+ body: 'Bio de redes, fichas de App Store/Google Play, npm, firmas de correo y enlaces dentro de la app: todos con la misma URL (con o sin www, la que elegiste). Son las primeras señales que Google usa para entender que ese dominio eres tú.',
486
+ },
487
+ {
488
+ key: 'generic_name',
489
+ title: 'Si tu marca es una palabra común',
490
+ body: 'Con un nombre genérico (una palabra del diccionario) primero vas a aparecer por "marca + producto" o por tu dominio, no por la palabra sola: compites con diccionarios y sitios con años de antigüedad. Lo que acelera: enlaces de sitios reconocidos (tiendas, npm, directorios, artículos) y contenido propio.',
491
+ },
488
492
  {
489
493
  key: 'privacy_legal',
490
494
  title: 'Privacidad y consentimiento',
@@ -495,7 +499,7 @@ export const PLATFORM_PLAYBOOKS = {
495
499
  tasks: [
496
500
  {
497
501
  title: 'Agregar el sitio a Google Search Console y enviar el sitemap',
498
- description: 'Crear la propiedad (tipo Dominio, verificación por DNS TXT), enviar sitemap.xml y confirmar que no marque errores de indexación. Sin esto no hay forma de saber si Google ve el sitio.',
502
+ description: 'Crear la propiedad (tipo Dominio, verificación por DNS TXT), enviar sitemap.xml y confirmar que no marque errores de indexación. Sin esto no hay forma de saber si Google ve el sitio. Al terminar, márcalo como hecho en Buenas prácticas.',
499
503
  type: 'tech', priority: 'high', tags: ['seo', 'web', 'search-console'], offsetDays: 1,
500
504
  },
501
505
  {
@@ -508,35 +512,20 @@ export const PLATFORM_PLAYBOOKS = {
508
512
  description: 'Solo si habrá anuncios en Facebook/Instagram: crear el Pixel en Events Manager, instalarlo en el sitio y confirmar PageView con la extensión Meta Pixel Helper o la pestaña Probar eventos.',
509
513
  type: 'tech', priority: 'low', tags: ['seo', 'web', 'analytics', 'meta'], offsetDays: 4, checkKey: 'meta_pixel',
510
514
  },
511
- {
512
- title: 'Verificar que sitemap.xml y robots.txt respondan 200 y no bloqueen el sitio',
513
- description: 'Abrirlos en el navegador en el dominio de producción. Revisar que robots.txt no tenga "Disallow: /" heredado de staging y que apunte al sitemap. En SPAs, comprobar que el hosting no devuelva index.html para estas rutas.',
514
- type: 'tech', priority: 'high', tags: ['seo', 'web'], offsetDays: 2,
515
- },
516
- {
517
- title: 'Revisar título, meta description, h1 y canonical de cada página indexable',
518
- description: 'Un <title> único (≤60) y meta description (≤155) por página, un solo h1 y canonical. Si es SPA, confirmar que cada ruta los entrega al rastreador (prerender/SSR o manejador de head).',
519
- type: 'tech', priority: 'medium', tags: ['seo', 'web'], offsetDays: 3,
520
- },
521
- {
522
- title: 'Configurar Open Graph / Twitter Cards con imagen 1200x630',
523
- description: 'og:title, og:description, og:image y twitter:card. Probar el enlace pegándolo en WhatsApp, LinkedIn y X.',
524
- type: 'business', priority: 'medium', tags: ['seo', 'web', 'social'], offsetDays: 3,
525
- },
526
515
  {
527
516
  title: 'Medir y corregir Core Web Vitals en PageSpeed Insights (móvil)',
528
517
  description: 'Correr PageSpeed Insights sobre la home y las páginas clave. Objetivo: LCP < 2.5s, CLS < 0.1, INP < 200ms. Atacar primero imágenes sin comprimir, fuentes bloqueantes y JS sin dividir.',
529
518
  type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5, checkKey: 'core_web_vitals',
530
519
  },
531
520
  {
532
- title: 'Fijar dominio canónico y redirecciones 301 (www / http → https)',
533
- description: 'Elegir una sola versión del dominio y redirigir las demás con 301, sin cadenas de redirecciones. Confirmar HTTPS válido.',
534
- type: 'tech', priority: 'medium', tags: ['seo', 'web'], offsetDays: 3,
521
+ title: 'Revisar en Search Console la canónica que eligió Google y las páginas sin indexar',
522
+ description: '1-2 semanas después de lanzar (o de cambiar el dominio): inspeccionar la home y confirmar que "Google-selected canonical" sea tu URL canónica, y revisar Páginas → "Por qué no se indexan". Al terminar, márcalo como hecho en Buenas prácticas.',
523
+ type: 'tech', priority: 'high', tags: ['seo', 'web', 'search-console'], offsetDays: 14,
535
524
  },
536
525
  {
537
- title: 'Agregar datos estructurados (JSON-LD) y validarlos',
538
- description: 'Organization como mínimo, más SoftwareApplication/FAQPage/Product según el sitio. Validar con la prueba de resultados enriquecidos de Google.',
539
- type: 'tech', priority: 'low', tags: ['seo', 'web'], offsetDays: 7,
526
+ title: 'Apuntar tus perfiles y fichas a la URL canónica',
527
+ description: 'Bio de redes, fichas de las tiendas, npm, firmas de correo y enlaces dentro de la app con la misma URL canónica. Al terminar, márcalo como hecho en Buenas prácticas.',
528
+ type: 'business', priority: 'medium', tags: ['seo', 'web'], offsetDays: 7,
540
529
  },
541
530
  {
542
531
  title: 'Importar la propiedad a Bing Webmaster Tools',
package/src/server.mjs CHANGED
@@ -146,7 +146,7 @@ No le preguntes al usuario por workspaceId ni projectId -- ya están configurado
146
146
 
147
147
  No crees tareas ni entradas de changelog por preguntas triviales o exploración sin cambios reales -- solo por trabajo que de verdad avanza el proyecto.
148
148
 
149
- Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene código (no un "vacío" recién conectado, ver arriba) y hace rato no se corrió este chequeo, o el usuario pide explícito "revisa fundamentos"/"qué le falta a este proyecto" (no lo hagas en cada mensaje ni en exploración sin cambios): primero llamá a get_project_findings -- ya trae, calculado por el servidor y priorizado, lo que falta o está mal dentro de Orquestra (vencidas, bloqueadas, módulos en riesgo, paridad, infraestructura, servicios que faltan, duplicadas) -- y preséntaselo al usuario por severidad antes de seguir; luego, para lo que solo se ve en el código, llamá a get_foundations_checklist y, con acceso real al filesystem del repo (grep de dependencias, archivos de config, workflows de CI -- seguí el detectionHint de cada ítem), confirmá cuáles de verdad faltan -- filtrá por "scales" contra el scale real del proyecto (ver project.scale en get_project_context; un ítem con scales:["platform"] no aplica a una landing). Por cada uno que confirmes ausente con evidencia concreta, llamá a report_foundation_gap con esa evidencia específica -- nunca lo reportes solo porque el checklist lo menciona, sin haber revisado el código de verdad. No repitas un chequeo ya hecho recientemente en la misma sesión.
149
+ Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene código (no un "vacío" recién conectado, ver arriba) y hace rato no se corrió este chequeo, o el usuario pide explícito "revisa fundamentos"/"qué le falta a este proyecto" (no lo hagas en cada mensaje ni en exploración sin cambios): primero llamá a get_project_findings -- ya trae, calculado por el servidor y priorizado, lo que falta o está mal dentro de Orquestra (vencidas, bloqueadas, módulos en riesgo, paridad, infraestructura, servicios que faltan, duplicadas) -- y preséntaselo al usuario por severidad antes de seguir; luego, para lo que solo se ve en el código, llamá a get_foundations_checklist y, con acceso real al filesystem del repo (grep de dependencias, archivos de config, workflows de CI -- seguí el detectionHint de cada ítem), confirmá cuáles de verdad faltan -- filtrá por "scales" contra el scale real del proyecto (ver project.scale en get_project_context; un ítem con scales:["platform"] no aplica a una landing). Antes, mira get_practices sin distributionId: los ítems con detectedByServer ya los revisa el servidor en los repos conectados, así que concéntrate en los que salen "unknown". Reporta en UNA llamada a report_foundations lo que confirmaste (pass si existe y se usa, fail si falta, na con motivo si el usuario decidió no tenerlo), con evidencia específica de archivos reales -- nunca lo reportes solo porque el checklist lo menciona, sin haber revisado el código de verdad. No repitas un chequeo ya hecho recientemente en la misma sesión.
150
150
 
151
151
  Si el usuario pregunta explícito qué Claude Skills de terceros le faltan (o pide instalar alguna) -- no lo hagas sin que te lo pidan: llamá a get_skills_catalog y revisá con acceso real al filesystem cuáles de esos keys ya existen en .claude/skills/ (del proyecto) o ~/.claude/skills/ (global). Antes de instalar cualquiera con install_skill, llamá primero a preview_skill y mostrale al usuario el skillMdContent completo -- nunca instales sin que haya visto ese contenido y confirmado explícitamente, los SKILL.md de terceros pueden traer instrucciones que un agente ejecuta solo con cargarlas.`
152
152
 
@@ -293,7 +293,7 @@ export function createServer() {
293
293
  registerRead(
294
294
  'search',
295
295
  '/v1/search',
296
- 'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra. También por folio ("ORQ-123", la exacta sale primero) y sin importar acentos. Primero lo que coincide en el folio o el título, después en el cuerpo; tope de 30 resultados: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
296
+ 'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra. También por folio ("ORQ-123") o por clave de módulo ("AUTH-3") -- la exacta sale primero -- y sin importar acentos. Primero lo que coincide en el folio o el título, después en el cuerpo; tope de 30 resultados: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
297
297
  {
298
298
  q: z.string().describe('Texto a buscar'),
299
299
  types: z.array(z.enum(['task', 'activity', 'note', 'idea', 'doc', 'knowledge'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en todos. knowledge = solo la base vinculada a este proyecto.'),
@@ -373,6 +373,79 @@ export function createServer() {
373
373
  { distributionId: z.string().describe('Distribución type="web" con domain configurado') }
374
374
  )
375
375
 
376
+ // Buenas prácticas (ORQ-392/394; tiendas iOS/Android ORQ-398): checklist que
377
+ // evalúa el servidor (lib/practices.mjs y lib/storePractices.mjs en infra) y
378
+ // guarda en `distributions.practices`. No hay
379
+ // GET propio: se lee de la distribución. Los puntos manuales los confirma una
380
+ // persona en la web -- a propósito no hay tool para marcarlos.
381
+ server.registerTool(
382
+ 'get_practices',
383
+ {
384
+ description: 'Checklist de buenas prácticas de una distribución, evaluado por el servidor cada lunes. Web: redirecciones 301 (http→https, con/sin www), robots.txt, sitemap, noindex, contenido en el HTML sin JS, título/descripción, h1, canonical, og:image, JSON-LD y Core Web Vitals; para la app (indexable=false) solo https + noindex. iOS/Android: ficha pública en App Store/Google Play, que la ficha enlace a la web canónica, frescura de la versión, capturas y calificación, y que las URLs de privacidad/soporte respondan 200. Cada punto trae status (pass|warn|fail|unknown|manual|na), severity, detail y evidence ya redactados -- úsalos tal cual, no re-diagnostiques. Los puntos group="manual" (Search Console, Data Collection/Data safety, cuenta demo del revisor...) los confirma el usuario en la web. Úsala después de un deploy o de llenar la guía de publicación (antes, run_practices) o cuando el usuario pregunte por SEO, indexación o la ficha de la tienda. Para corregir un fail, trabaja en el código real del repo o en la guía de publicación y vuelve a revisar. SIN distributionId devuelve los checklists del PROYECTO, revisados a diario: FUNDAMENTOS (proyectos de código en construcción o lanzados: distribuciones, servicios, entornos y repos registrados, canal de beta, agente de QA, bugs de prioridad alta abiertos, y el grupo Código -- README/CI/Dependabot los ve el servidor en los repos conectados, analytics/crash_reporting/error_alerting solo con report_foundations) y CRECIMIENTO (etapa post): canales registrados y con ritmo, ritmo cumplido por canal en 28 días, contenido programado a 7 días, changelog reciente, última versión anunciada en Contenido, IDs de medición y buenas prácticas de la web pública -- para corregir un fail de ritmo crea piezas reales con create_content en canales de list_channels, nunca inventes un canal.',
385
+ inputSchema: {
386
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
387
+ distributionId: z.string().optional().describe('Distribución type="web", "ios" o "android" (ver list_distributions). Omitido = checklist de crecimiento del proyecto.'),
388
+ },
389
+ },
390
+ async ({ projectId, distributionId }) => {
391
+ try {
392
+ if (!distributionId) {
393
+ const p = await apiGet(config, `/v1/projects/${resolveProjectId(projectId)}`, {})
394
+ return textResult({
395
+ projectId: p.id, name: p.name, phase: p.phase,
396
+ foundations: p.foundations || null,
397
+ foundationsManual: p.foundationsManual || {},
398
+ foundationsCode: p.foundationsCode || {},
399
+ growth: p.growth || null,
400
+ growthManual: p.growthManual || {},
401
+ hint: [
402
+ !p.foundations && 'Fundamentos: se revisa en proyectos de código en construcción (mvp/alpha/beta) o ya lanzados.',
403
+ !p.growth && 'Crecimiento: solo en la etapa post (fase launch/v1/v2/v3/mantenimiento) o con el playbook post aplicado.',
404
+ ].filter(Boolean).join(' ') || undefined,
405
+ })
406
+ }
407
+ const { items } = await apiGet(config, '/v1/distributions', { projectId: resolveProjectId(projectId) })
408
+ const d = (items || []).find(x => x.id === distributionId)
409
+ if (!d) return errorResult(new Error(`No existe la distribución ${distributionId} en este proyecto`))
410
+ const store = d.type === 'ios' || d.type === 'android'
411
+ if (d.type !== 'web' && !store) return errorResult(new Error('Las buenas prácticas solo aplican a distribuciones type="web", "ios" o "android"'))
412
+ return textResult({
413
+ id: d.id, name: d.name || d.label, type: d.type,
414
+ ...(store ? { bundleId: d.bundleId || d.identifier || null } : { domain: d.domain || null, indexable: d.indexable ?? null }),
415
+ practices: d.practices || null,
416
+ practicesManual: d.practicesManual || {},
417
+ ...(!store && !d.domain ? { hint: 'Sin domain configurado no se revisa: agrégalo con update_distribution.' } : {}),
418
+ ...(store && !d.bundleId && !d.identifier ? { hint: 'Sin bundleId no se encuentra la ficha de la tienda: agrégalo con update_distribution.' } : {}),
419
+ })
420
+ } catch (err) {
421
+ return errorResult(err)
422
+ }
423
+ }
424
+ )
425
+
426
+ server.registerTool(
427
+ 'run_practices',
428
+ {
429
+ description: 'Pide una revisión nueva de buenas prácticas para una distribución web con dominio o una app iOS/Android (por ejemplo, justo después de desplegar o de publicar en la tienda). Corre en segundo plano (unos segundos): responde "running"; el resultado aparece después en get_practices. 409 si ya hay una en curso. SIN distributionId recalcula al momento los dos checklists del proyecto (fundamentos y crecimiento) y devuelve {foundations, growth}; el que no aplica a la fase del proyecto viene como {skipped}. Si un punto que pasaba ahora falla, el servidor avisa solo al equipo.',
430
+ inputSchema: {
431
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
432
+ distributionId: z.string().optional().describe('Distribución type="web" con domain configurado, o type="ios"/"android". Omitido = checklist de crecimiento del proyecto.'),
433
+ },
434
+ },
435
+ async ({ projectId, distributionId }) => {
436
+ try {
437
+ const pid = resolveProjectId(projectId)
438
+ if (distributionId) return textResult(await apiPost(config, '/v1/practices', { projectId: pid, distributionId }))
439
+ // Sin distribución: los dos checklists del proyecto; el que no aplica responde 400.
440
+ const run = path => apiPost(config, path, { projectId: pid }).catch(err => ({ skipped: err.message }))
441
+ const [foundations, growth] = await Promise.all([run('/v1/foundations'), run('/v1/growth')])
442
+ return textResult({ foundations, growth })
443
+ } catch (err) {
444
+ return errorResult(err)
445
+ }
446
+ }
447
+ )
448
+
376
449
  registerRead(
377
450
  'get_infra_status',
378
451
  '/v1/infra',
@@ -641,32 +714,30 @@ export function createServer() {
641
714
  server.registerTool(
642
715
  'get_foundations_checklist',
643
716
  {
644
- description: 'Checklist de "fundamentos olvidados" -- prácticas chicas (analytics, crash reporting, alertas de error, escaneo de dependencias, CI) que casi ningún proyecto instrumenta por su cuenta pero suman mucho cuando faltan. No llama a la API -- datos estáticos. Cada ítem trae "scales" (a qué project.scale aplica -- "landing" queda afuera de la mayoría a propósito) y "detectionHint" para que TÚ (con acceso real al filesystem del repo) confirmes si de verdad falta antes de reportarlo con report_foundation_gap -- ver SERVER_INSTRUCTIONS para cuándo correr este chequeo.',
717
+ description: 'Checklist de "fundamentos olvidados" -- prácticas chicas (README, CI, escaneo de dependencias, analytics, crash reporting, alertas de error) que casi ningún proyecto instrumenta por su cuenta pero suman mucho cuando faltan. No llama a la API -- datos estáticos. Cada ítem trae "scales" (a qué project.scale aplica -- "landing" queda afuera de la mayoría a propósito), "detectionHint" para que TÚ (con acceso real al filesystem del repo) confirmes si se cumple, y "detectedByServer" (true = el servidor ya lo revisa solo en los repos conectados; no hace falta reportarlo salvo para marcar na o si no hay repo conectado). Reporta lo confirmado con report_foundations; el estado actual está en get_practices sin distributionId.',
645
718
  },
646
719
  async () => textResult(FOUNDATIONS)
647
720
  )
648
721
 
722
+ // ORQ-400: reemplaza a report_foundation_gap (creaba una sugerencia suelta). Ahora lo
723
+ // confirmado queda en el checklist de fundamentos del proyecto (project.foundationsCode),
724
+ // tanto lo que se cumple como lo que falta, y la web lo pinta en el Dashboard.
649
725
  server.registerTool(
650
- 'report_foundation_gap',
726
+ 'report_foundations',
651
727
  {
652
- description: 'Reporta un ítem de get_foundations_checklist que confirmaste ausente con evidencia real del repo (no lo llames sin haber revisado el código de verdad). Crea una sugerencia "foundation_gap", visible en el drawer "Sugerencias de IA" de la web -- aceptarla crea una tarea real (mismo mecanismo que accept_suggestion). Si ese foundationKey ya tiene una sugerencia (pending, aceptada o descartada) no crea una duplicada -- descartar una no la hace reaparecer en el siguiente chequeo.',
728
+ description: 'Reporta al checklist de fundamentos del proyecto lo que confirmaste en el código real de los repos (ítems de get_foundations_checklist): pass si existe Y se usa, fail si falta, na si no aplica (con el motivo). Nunca lo reportes sin haber revisado los archivos de verdad. Queda vigente 90 días y gana sobre lo que el servidor detecta solo en el repo, así que úsalo sobre todo para analytics/crash_reporting/error_alerting (el servidor no lee código) y para marcar na lo que el usuario decidió no tener. Recalcula el checklist y devuelve el resultado completo; un fail aparece en el Dashboard con "Crear tarea".',
653
729
  inputSchema: {
654
730
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
655
- foundationKey: z.string().describe('key del ítem de get_foundations_checklist, ej. "analytics"'),
656
- title: z.string().describe('Título corto y específico al proyecto real, ej. "Falta Crashlytics en la app iOS" -- no copies el label genérico del checklist tal cual.'),
657
- reason: z.string().describe('Evidencia concreta que encontraste (qué buscaste, qué NO encontraste) y por qué importa acá -- no una explicación genérica del ítem.'),
731
+ reports: z.array(z.object({
732
+ key: z.enum(FOUNDATIONS.map(f => f.key)).describe('key del ítem de get_foundations_checklist'),
733
+ status: z.enum(['pass', 'fail', 'na']),
734
+ evidence: z.string().min(1).describe('Evidencia concreta y específica del proyecto: archivo y qué encontraste (ej. "Crashlytics.crashlytics() en AppDelegate.swift, sin equivalente en la web"), o el motivo de na. Nada genérico.'),
735
+ })).min(1),
658
736
  },
659
737
  },
660
- async ({ projectId, foundationKey, title, reason }) => {
738
+ async ({ projectId, reports }) => {
661
739
  try {
662
- const resolvedProjectId = resolveProjectId(projectId)
663
- const data = await apiPost(config, '/v1/suggestions', {
664
- projectId: resolvedProjectId,
665
- kind: 'foundation_gap',
666
- foundationKey, title, reason,
667
- idempotencyKey: `foundation:${resolvedProjectId}:${foundationKey}`,
668
- })
669
- return textResult(data)
740
+ return textResult(await apiPost(config, '/v1/foundations', { projectId: resolveProjectId(projectId), reports }))
670
741
  } catch (err) {
671
742
  return errorResult(err)
672
743
  }
@@ -697,6 +768,7 @@ export function createServer() {
697
768
  skipParity: z.boolean().optional().describe('true = NO sumar las plataformas hermanas al scope aunque tengan el mismo productKey. Solo cuando el trabajo es genuinamente de UNA plataforma (ej. un ajuste de Gradle que no tiene contraparte en iOS) -- no lo mandes por default. Las tareas type="bug" nunca propagan, sin necesidad de esto.'),
698
769
  parityExempt: z.boolean().optional().describe('true = este trabajo es de UNA sola plataforma y no cuenta en get_parity_summary (se pone solo cuando skipParity va con scope de una sola dist). false para volver a contarla.'),
699
770
  parityOf: z.string().optional().describe('Folio: id de la tarea ORIGEN de la que esta es la hermana para otra plataforma (ej. la versión iOS de una tarea de Android). En paridad cuentan como UN item y al cerrar esta, su plataforma queda hecha en el origen. El origen no puede ser hermana de otra. "" lo quita.'),
771
+ moduleCode: z.string().max(40).optional().describe('Clave de la tarea dentro de su módulo (ej. "AUTH-3"), ADEMÁS del folio ORQ-N. NO la mandes normalmente: si el módulo tiene clave (key), el server la asigna sola. Solo para conservar la clave que ya traía de otro sistema al importar (ej. "PROJ-128" de Jira/Linear).'),
700
772
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea. LEGACY desde ORQ-248: si vas a bloquear por más de una tarea, usa blockedByIds en vez de esto (relatedTaskId se sigue llenando solo, como blockedByIds[0], para paridad con iOS/Android).'),
701
773
  blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- ids de otras tareas (puede ser más de una). Esta tarea se muestra como bloqueada mientras ALGUNA de esas no llegue a un estatus final (isFinal). Reemplaza a relatedTaskId para bloqueos múltiples -- preferí este campo, no relatedTaskId, salvo que de verdad sea un solo bloqueador.'),
702
774
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call" (plataforma o lugar, ej. "Zoom", "Oficina") o "followup"/"business" (contexto libre) -- ignóralo para el resto de los tipos.'),
@@ -742,6 +814,7 @@ export function createServer() {
742
814
  .describe('Alcance por distribución -- usa get_parity_summary para ver el scope actual antes de editarlo'),
743
815
  parityExempt: z.boolean().optional().describe('true = este trabajo es de UNA sola plataforma y no cuenta en get_parity_summary; false para volver a contarla.'),
744
816
  parityOf: z.string().optional().describe('Folio: id de la tarea ORIGEN de la que esta es la hermana para otra plataforma (ej. la versión iOS de una tarea de Android). En paridad cuentan como UN item y al cerrar esta, su plataforma queda hecha en el origen. El origen no puede ser hermana de otra. "" lo quita.'),
817
+ moduleCode: z.string().max(40).optional().describe('Clave de la tarea dentro de su módulo (ej. "AUTH-3"), texto libre editable a mano. "" la quita. Mover la tarea a otro módulo (moduleId) NO la cambia: la clave es estable.'),
745
818
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. LEGACY, ver create_task -- preferí blockedByIds.'),
746
819
  blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- reemplaza el array COMPLETO (no hace merge), [] quita todos los bloqueos. Ver create_task.'),
747
820
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call"/"followup"/"business". Ver create_task.'),
@@ -2266,6 +2339,7 @@ export function createServer() {
2266
2339
 
2267
2340
  const SCOPE_SCHEMA = z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean().optional(), ...SCOPE_EVIDENCE })).optional()
2268
2341
 
2342
+ const MODULE_KEY = z.string().optional().describe('Clave del módulo (ej. "AUTH", "PAGOS"): 2-10 letras/números, empieza con letra, única en el proyecto. Numera sus tareas (AUTH-1, AUTH-2...) en moduleCode, además del folio ORQ-N. Solo si el usuario la pidió o el proyecto ya usa claves -- no la inventes.')
2269
2343
  registerCreate('create_module', '/v1/modules', 'Crea un módulo en un proyecto de Orquestra.', {
2270
2344
  name: z.string().describe('Nombre del módulo'),
2271
2345
  description: z.string().optional(),
@@ -2278,6 +2352,7 @@ export function createServer() {
2278
2352
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2279
2353
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
2280
2354
  closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
2355
+ key: MODULE_KEY,
2281
2356
  })
2282
2357
  registerUpdate('update_module', '/v1/modules', 'Edita un módulo existente (incluido su scope multiplataforma).', {
2283
2358
  name: z.string().optional(),
@@ -2291,6 +2366,7 @@ export function createServer() {
2291
2366
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2292
2367
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
2293
2368
  closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
2369
+ key: MODULE_KEY.describe(`${MODULE_KEY.description} Poner o cambiar la clave numera las tareas del módulo que todavía no tienen una (por orden de creación; la respuesta trae assignedCodes). "" la quita: las tareas conservan la suya.`),
2294
2370
  })
2295
2371
 
2296
2372
  registerCreate('create_status', '/v1/statuses', 'Crea un estatus (columna del flujo de tareas) en un proyecto.', {
@@ -2473,6 +2549,7 @@ export function createServer() {
2473
2549
  repoUrl: z.string().optional().describe('URL del repo de este código -- solo informativo, no requiere credenciales.'),
2474
2550
  knowledgeSource: z.string().optional().describe('Doc libre tipo CLAUDE.md para el agente de QA de esta distribución.'),
2475
2551
  publishForm: z.record(z.string()).optional().describe('Respuestas del formulario de la Guía de publicación -- clave = key de referenceGuide en get_platform_playbook, valor = texto declarado. Reemplaza el mapa completo (nunca merge parcial). Preferí apply_platform_playbook a mandar esto directo -- esa tool avisa y pide confirmación si la distribución ya tiene datos guardados, esta lo sobreescribe sin preguntar.'),
2552
+ indexable: z.boolean().nullable().optional().describe('Solo web: ¿debe aparecer en Google? true = web pública (landing), false = app/login (las buenas prácticas le exigen noindex), null = que el servidor lo infiera. Pregúntale al usuario si no es obvio; no lo adivines.'),
2476
2553
  })
2477
2554
 
2478
2555
  // ═══ Contenido (Fase 1 del rediseño, 11 ago 2026; modelo multi-canal
package/src/stages.mjs CHANGED
@@ -47,39 +47,29 @@ export const PLAYBOOKS = {
47
47
  },
48
48
  build: {
49
49
  moduleName: 'Camino a beta',
50
- moduleDescription: 'La base técnica y de proceso que necesitas para llegar a una beta estable con usuarios reales dando feedback.',
50
+ moduleDescription: 'El trabajo humano para llegar a una beta con usuarios reales dando feedback: entrada al producto, flujo de feedback y testers. Lo técnico que se puede revisar (infraestructura registrada, canal de beta, QA, bugs urgentes, README, CI, dependencias, analítica y crashes) lo revisa solo el checklist de Fundamentos del Dashboard.',
51
51
  moduleTags: ['playbook', 'proyecto'],
52
52
  intro: 'Construye con dirección: infraestructura medible, feedback desde el día uno y una beta que puedas poner en manos de usuarios reales.',
53
53
  tasks: [
54
- { title: 'Configurar entornos e infraestructura base', description: 'Backend, base de datos, auth y storage funcionando. Regístralos en la sección Infra con su proveedor y estado.', type: 'tech', priority: 'high', tags: ['infra'], offsetDays: 3 },
55
- { title: 'Instrumentar analytics y crash reporting', description: 'Desde el primer build: eventos clave (registro, acción principal, retorno) y reporte de crashes. Sin datos no hay decisiones.', type: 'tech', priority: 'high', tags: ['analytics'], offsetDays: 7 },
56
54
  { title: 'Implementar autenticación y onboarding', description: 'El flujo de entrada define la primera impresión. Registro simple + un onboarding que lleve al usuario a su primer momento de valor.', type: 'feature', priority: 'high', tags: ['onboarding'], offsetDays: 14 },
57
55
  { title: 'Definir flujo de feedback de beta testers', description: '¿Cómo te van a reportar bugs y sugerencias? (formulario, grupo de WhatsApp, botón in-app). Decide dónde aterriza: aquí en Ideas.', type: 'improvement', priority: 'medium', tags: ['beta', 'feedback'], offsetDays: 18 },
58
- { title: 'Preparar build de beta (TestFlight / track interno / staging)', description: 'Un canal de distribución de beta por plataforma. Registra la versión en review en el panel de Infra.', type: 'tech', priority: 'high', tags: ['beta'], offsetDays: 21 },
59
56
  { title: 'Reclutar 10-20 beta testers', description: 'Idealmente del público objetivo que validaste en pre-proyecto. Amigos que no son el público te dirán que "está padre" y nada más.', type: 'business', priority: 'medium', tags: ['beta'], offsetDays: 25 },
60
- { title: 'Documentar setup del proyecto (README)', description: 'Cómo levantar el proyecto desde cero: dependencias, variables de entorno, comandos. Guárdalo en Docs. Tu yo del futuro lo agradece.', type: 'docs', priority: 'low', tags: ['docs'], offsetDays: 28 },
61
- { title: 'Checklist de estabilidad pre-lanzamiento', description: 'Crashes < 1%, flujos principales sin bugs conocidos, tiempos de carga aceptables, datos de prueba limpios. Si pasa, estás listo para launch.', type: 'tech', priority: 'high', tags: ['calidad'], offsetDays: 35 },
62
57
  ],
63
58
  },
64
59
  post: {
65
60
  moduleName: 'Lanzamiento y crecimiento',
66
- moduleDescription: 'El plan de marketing paso a paso después de la beta: posicionamiento, canales de lanzamiento, contenido, y el loop de métricas y feedback que convierte un lanzamiento en crecimiento.',
61
+ moduleDescription: 'El plan de lanzamiento después de la beta: posicionamiento, landing, canales y contenido de lanzamiento. Lo que se sostiene en el tiempo (ritmo de publicación, changelog anunciado, analítica, feedback y revisión semanal de métricas) lo revisa solo el checklist de Crecimiento del Dashboard.',
67
62
  moduleTags: ['playbook', 'post-proyecto', 'marketing'],
68
63
  intro: 'Terminaste la beta — ahora el trabajo es que la gente lo use. Este playbook te lleva del "¿y ahora qué?" a un lanzamiento con canales, contenido y métricas.',
69
64
  tasks: [
70
65
  { title: 'Redactar el pitch: propuesta de valor en una frase', description: 'La frase que usarás en la tienda, la landing y redes. Fórmula: "[Producto] ayuda a [público] a [resultado] sin [dolor]". Itérala hasta que alguien ajeno la entienda a la primera.', type: 'business', priority: 'high', tags: ['marketing', 'posicionamiento'], offsetDays: 2 },
71
66
  { title: 'Preparar assets de tienda (ASO): capturas, descripción, keywords', description: '5-8 capturas que cuenten una historia (no pantallas sueltas), descripción con el pitch arriba, y keywords que tu público realmente busca.', type: 'business', priority: 'high', tags: ['marketing', 'aso'], offsetDays: 5 },
72
67
  { title: 'Publicar landing con captura de correos', description: 'Una página: pitch, 3 beneficios, capturas y un CTA (descargar o dejar correo). Los correos capturados son tu canal propio para siempre.', type: 'feature', priority: 'high', tags: ['marketing', 'landing'], offsetDays: 7 },
73
- { title: 'Configurar analytics de adquisición', description: 'Fuentes de tráfico, conversión de visita→registro→activación. Necesitas saber DE DÓNDE llegan los usuarios que sí se quedan.', type: 'tech', priority: 'medium', tags: ['analytics', 'adquisición'], offsetDays: 8 },
74
68
  { title: 'Conectar tus canales reales de contenido', description: 'Antes de programar nada: registra en Contenido → Cuentas conectadas los canales donde de verdad vas a publicar (redes, blog, newsletter). El plan de contenido de abajo solo sirve con canales reales, no uno genérico — si personalizas este playbook con IA y ya tienes canales conectados, te genera piezas reales directo.', type: 'tech', priority: 'high', tags: ['marketing', 'contenido'], offsetDays: 9 },
75
69
  { title: 'Plan de contenido de lanzamiento (2 semanas)', description: 'Con tus canales ya conectados (tarea anterior), programa en Contenido: teaser, anuncio, demo del caso de uso principal, historia de por qué lo construiste, testimonios de beta testers — uno por canal real, no una lista genérica.', type: 'business', priority: 'high', tags: ['marketing', 'contenido'], offsetDays: 10 },
76
70
  { title: 'Lanzar en canales: Product Hunt, comunidades y redes', description: 'Elige 3 canales donde vive tu público (PH, subreddits, grupos de FB/Discord, foros del nicho). Un post honesto de creador funciona mejor que uno de venta.', type: 'business', priority: 'high', tags: ['marketing', 'lanzamiento'], offsetDays: 14 },
77
71
  { title: 'Contactar 10 personas con audiencia en el nicho', description: 'Creators, newsletters, podcasts o prensa pequeña del tema. Mensaje corto y personalizado + acceso gratis. Con que respondan 2, ganaste.', type: 'followup', priority: 'medium', tags: ['marketing', 'outreach'], offsetDays: 16 },
78
- { title: 'Activar loop de feedback de usuarios reales', description: 'Un canal permanente: encuesta in-app, correo de bienvenida que pregunta "¿qué te trajo aquí?", o botón de sugerencias. Todo aterriza en Ideas.', type: 'improvement', priority: 'medium', tags: ['feedback'], offsetDays: 18 },
79
72
  { title: 'Definir y medir activación y retención (D1 / D7 / D30)', description: 'Activación: % que llega al momento de valor. Retención: % que regresa al día 1, 7 y 30. Estos números te dicen si tienes producto o solo descargas.', type: 'business', priority: 'medium', tags: ['métricas'], offsetDays: 21 },
80
- { title: 'Ritmo de contenido post-lanzamiento (3 publicaciones/semana)', description: 'El lanzamiento es un día; el crecimiento es constancia. Deja programadas en Contenido las siguientes 2 semanas: tips de uso, casos, mejoras del changelog — repartidas entre tus canales reales conectados.', type: 'business', priority: 'medium', tags: ['marketing', 'contenido'], offsetDays: 24 },
81
- { title: 'Publicar release notes / changelog público v1', description: 'Convierte tu Changelog en comunicación: qué hay de nuevo, en lenguaje de usuario. Cada release es una excusa para volver a aparecer.', type: 'docs', priority: 'low', tags: ['changelog'], offsetDays: 26 },
82
- { title: 'Revisión semanal de métricas y próximos pasos', description: 'Reunión recurrente (aunque seas solo tú): adquisición, activación, retención, top feedback. Decide UNA mejora prioritaria por semana.', type: 'meeting', priority: 'medium', tags: ['métricas', 'ritmo'], offsetDays: 28 },
83
73
  ],
84
74
  },
85
75
  }