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 +1 -1
- package/src/foundations.mjs +18 -2
- package/src/platformPlaybooks.mjs +39 -50
- package/src/server.mjs +94 -17
- package/src/stages.mjs +2 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "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": {
|
package/src/foundations.mjs
CHANGED
|
@@ -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
|
|
8
|
-
//
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
533
|
-
description: '
|
|
534
|
-
type: 'tech', priority: '
|
|
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: '
|
|
538
|
-
description: '
|
|
539
|
-
type: '
|
|
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).
|
|
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"
|
|
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
|
|
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
|
-
'
|
|
726
|
+
'report_foundations',
|
|
651
727
|
{
|
|
652
|
-
description: 'Reporta
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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,
|
|
738
|
+
async ({ projectId, reports }) => {
|
|
661
739
|
try {
|
|
662
|
-
|
|
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: '
|
|
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
|
|
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
|
}
|