orquestra-mcp 1.6.5 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.6.5",
4
- "description": "Servidor MCP de Orquestra -- 38 tools para leer y escribir tareas, ideas, changelog, infraestructura, QA y más desde un agente de IA",
3
+ "version": "1.7.1",
4
+ "description": "Servidor MCP de Orquestra -- 51 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": {
7
7
  "orquestra-mcp": "./bin/orquestra-mcp.mjs"
package/src/server.mjs CHANGED
@@ -14,10 +14,11 @@ function textResult(data) {
14
14
 
15
15
  const PROJECT_ID_DESC = 'ID del proyecto dentro del workspace configurado. Si hay un proyecto default configurado (via "setup" o ORQUESTRA_DEFAULT_PROJECT_ID), este argumento se ignora -- el server queda fijo a ese proyecto y no puede leer/escribir otro.'
16
16
 
17
- const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'meeting', 'call', 'followup', 'business']
17
+ const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'design', 'security', 'meeting', 'call', 'followup', 'business']
18
18
  const TASK_PRIORITIES = ['high', 'medium', 'low']
19
19
  const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
20
20
  const DOC_TYPES = ['readme', 'usecase', 'diagram', 'env', 'adr', 'runbook', 'api']
21
+ const ADR_STATUSES = ['propuesto', 'aceptado', 'deprecado', 'rechazado']
21
22
  const SERVICE_TYPES = ['backend', 'database', 'auth', 'storage', 'push', 'email', 'payments', 'analytics', 'realtime', 'search', 'maps', 'flags', 'admin', 'cdn', 'queue']
22
23
  const PLATFORM_TYPES = ['android', 'ios', 'web', 'desktop-mac', 'desktop-win', 'backend', 'extension', 'other']
23
24
  const INFRA_STATUSES = ['operational', 'degraded', 'down']
@@ -62,6 +63,13 @@ Durante el trabajo normal de código, sin que el usuario te lo pida explícitame
62
63
  - Si tienes acceso al repo real de una distribución (ej. puedes correr "git remote get-url origin" en ese código), llena repoUrl en create_distribution/update_distribution sin preguntar -- es solo informativo, no requiere credenciales. Esto es distinto de conectar el repo con PAT para el changelog automático (Workspace → Conexiones en orquestra-web): eso sí es un paso deliberado del usuario, con una credencial real -- nunca lo hagas tú solo ni sugieras que ya está conectado por haber llenado repoUrl.
63
64
  - No todas las tareas necesitan una corrida de QA -- clasifica antes de disparar una: cambios que afectan comportamiento real (features, bugs, UI, lógica de negocio, endpoints/API) sí la necesitan; cambios que no alteran comportamiento (docs, comentarios, refactors sin cambio funcional, config/tooling interno, notas/reuniones/ideas) no. Si decides que sí, revisa primero con list_qa_agents que exista un agente para el stack que tocaste (si no hay ninguno, créalo con create_qa_agent -- necesita una máquina ya emparejada, ver Workspace → QA) y llama a create_qa_run en la rama real de ese trabajo (manda taskId si el trabajo era de una tarea puntual -- si el agente tiene la revisión con IA activada, la acota a eso en vez de "revisa todo el proyecto"), dejando la tarea en "En revisión" -- NUNCA la muevas a "Listo" ni digas que "QA lo revisó" sin haber confirmado con get_qa_run un estatus terminal "passed" de verdad (las corridas son asíncronas, tardan minutos en otra máquina). Si tarda, dile al usuario que quedó en cola (con el runId) y que confirmas cuando termine -- no lo des por hecho nunca.
64
65
 
66
+ Si el usuario pide "sincronizar"/"revisar que todo esté al día"/"verificar el proyecto" (o algo equivalente -- una auditoría manual, no el trabajo normal de arriba), hacé una "revisión de sincronización" completa, de cero, no un vistazo rápido:
67
+ 1. Llamá a get_sync_report -- devuelve lo objetivo (tareas sin moduleId, tareas activas sin dueDate, referencias huérfanas a un módulo/categoría/tarea/estatus que ya no existe) y activeTasksSnapshot (TODAS las tareas activas, no solo las incompletas).
68
+ 2. Para cada tarea en tasksMissingModule: buscá con list_modules si hay uno que aplique de verdad; si no, create_module antes de asignarlo -- nunca dejes una sin módulo (ver moduleId obligatorio en create_task, esto es limpiar deuda vieja de antes de esa regla).
69
+ 3. Para CADA tarea de activeTasksSnapshot (no solo las que tienen dueDate/priority en blanco -- una prioridad "medium" puesta por default nunca se distingue de una elegida a propósito): revisá el contexto real (código, conversación, changelog, dependencias vía relatedTaskId) y asigná una fecha límite realista y un nivel de urgencia real con update_task si el valor actual no refleja la urgencia/plazo real -- no inventes una fecha arbitraria sin ninguna base ("en 3 días" porque sí); si genuinamente no hay forma de estimar una fecha con la información disponible, dejala en blanco en vez de inventar una.
70
+ 4. Las referencias huérfanas (tasksWithOrphanModuleId/CategoryId/RelatedTaskId/StatusId) casi nunca se pueden arreglar solas con criterio -- no adivines a qué debería apuntar. Marcalas para el resumen final, y si es obvio por contexto a qué debió apuntar, proponéselo al usuario en vez de aplicarlo directo.
71
+ 5. Cerrá SIEMPRE con un resumen estructurado en 3 grupos, aunque algún grupo quede vacío: **No tiene sentido / necesita tu decisión** (referencias huérfanas, contradicciones que encontraste), **Se llenó** (campos que estaban vacíos y ahora tienen un valor real, con qué tarea y qué campo), **Se completó** (lo que quedó totalmente resuelto en esta pasada -- ej. una tarea que ya tenía todo en orden, o un hueco que se cerró del todo). Si tocaste tareas de verdad (no solo leíste el reporte), dejá rastro con un comment en cada una vía update_task explicando qué cambiaste y por qué -- mismo criterio que un cambio de statusId, no debería quedar invisible.
72
+
65
73
  No le preguntes al usuario por workspaceId ni projectId -- ya están configurados. Solo pregúntale por el proyecto si tiene más de uno y no queda claro a cuál te refieres.
66
74
 
67
75
  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.`
@@ -187,6 +195,14 @@ export function createServer() {
187
195
  (args, projectId) => ({ projectId })
188
196
  )
189
197
 
198
+ registerRead(
199
+ 'get_sync_report',
200
+ '/v1/sync-report',
201
+ 'Auditoría de integridad de un proyecto para una "revisión de sincronización" completa: tareas sin moduleId, tareas activas sin dueDate, y referencias huérfanas (moduleId/categoryId/relatedTaskId/statusId que ya no existen). También devuelve activeTasksSnapshot -- TODAS las tareas activas con su moduleId/priority/dueDate actuales, no solo las que tienen un campo en blanco, para que reevalúes fecha límite y urgencia con criterio real (ver flujo de "revisión de sincronización" en las instrucciones de este server).',
202
+ {},
203
+ (args, projectId) => ({ projectId })
204
+ )
205
+
190
206
  server.registerTool(
191
207
  'get_workspace_summary',
192
208
  {
@@ -276,7 +292,7 @@ export function createServer() {
276
292
  registerCreate(
277
293
  'create_task',
278
294
  '/v1/tasks',
279
- 'Crea una tarea en un proyecto de Orquestra.',
295
+ 'Crea una tarea en un proyecto de Orquestra. moduleId es OBLIGATORIO -- revisa list_modules primero; si ninguno aplica, creá uno con create_module antes de crear la tarea (nunca la dejes sin agrupar).',
280
296
  {
281
297
  title: z.string().describe('Título de la tarea'),
282
298
  description: z.string().optional(),
@@ -284,12 +300,26 @@ export function createServer() {
284
300
  priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium'),
285
301
  statusId: z.string().optional().describe('Default: el primer estatus del proyecto'),
286
302
  categoryId: z.string().optional(),
287
- moduleId: z.string().optional(),
303
+ moduleId: z.string().describe('OBLIGATORIO -- módulo al que pertenece esta tarea, para poder agruparla/revisarla después. Si no existe uno relacionado, llamá primero a list_modules (para no duplicar) y create_module.'),
288
304
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
289
305
  assignedTo: z.string().optional().describe('uid del miembro asignado'),
290
306
  tags: z.array(z.string()).optional(),
307
+ scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
308
+ .describe('Alcance por distribución (paridad multiplataforma) -- para setearlo desde la creación en vez de crear y después update_task. Usa get_parity_summary/list_distributions para ver qué distId existen.'),
291
309
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea. Esta tarea se muestra como bloqueada mientras esa otra no llegue a un estatus final (isFinal). Úsalo cuando el trabajo real depende de que otra tarea termine primero.'),
292
310
  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.'),
311
+ estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
312
+ stack: z.array(z.string()).optional().describe('Tecnologías involucradas (mismo vocabulario que el stack de una distribución, ej. "node", "react", "postgres").'),
313
+ branch: z.string().optional().describe('Branch de código asociado a esta tarea.'),
314
+ prNumber: z.string().optional().describe('Número del Pull Request asociado (sin el "#").'),
315
+ prUrl: z.string().optional(),
316
+ prStatus: z.string().optional().describe('"open" | "merged" | "closed", o lo que corresponda -- texto libre.'),
317
+ commitCount: z.number().optional(),
318
+ lastCommitAt: z.number().optional().describe('Timestamp en ms del último commit conocido.'),
319
+ resolvedCommitSha: z.string().optional().describe('SHA del commit que de verdad resolvió esta tarea, si ya lo sabes (ej. lo corriste tú). No lo inventes ni lo copies de otra tarea -- si no estás seguro, mejor déjalo vacío y usa "Actualizar desde GitHub" en el detalle (busca por shortCode).'),
320
+ resolvedCommitMessage: z.string().optional(),
321
+ resolvedCommitUrl: z.string().optional(),
322
+ resolvedCommitAt: z.number().optional().describe('Timestamp en ms del commit resolutor.'),
293
323
  }
294
324
  )
295
325
 
@@ -313,6 +343,85 @@ export function createServer() {
313
343
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. Ver create_task.'),
314
344
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call"/"followup"/"business". Ver create_task.'),
315
345
  comment: z.string().optional().describe('Explica qué hiciste o por qué cambia la tarea -- queda como comentario visible en ella. Obligatorio en la práctica cuando mandas statusId: sin esto, alguien ve que la tarea cambió de estatus pero no por qué.'),
346
+ estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
347
+ stack: z.array(z.string()).optional().describe('Tecnologías involucradas -- ver create_task.'),
348
+ branch: z.string().optional().describe('Branch de código asociado a esta tarea -- útil setearlo apenas empiezas a trabajar, así "Actualizar desde GitHub" en el detalle puede traer PR/commits reales después.'),
349
+ prNumber: z.string().optional().describe('Número del Pull Request asociado (sin el "#").'),
350
+ prUrl: z.string().optional(),
351
+ prStatus: z.string().optional().describe('"open" | "merged" | "closed", o lo que corresponda -- texto libre.'),
352
+ commitCount: z.number().optional(),
353
+ lastCommitAt: z.number().optional().describe('Timestamp en ms del último commit conocido.'),
354
+ resolvedCommitSha: z.string().optional().describe('SHA del commit que de verdad resolvió esta tarea, si ya lo sabes (ej. lo corriste tú). No lo inventes ni lo copies de otra tarea -- si no estás seguro, mejor déjalo vacío y usa "Actualizar desde GitHub" en el detalle (busca por shortCode).'),
355
+ resolvedCommitMessage: z.string().optional(),
356
+ resolvedCommitUrl: z.string().optional(),
357
+ resolvedCommitAt: z.number().optional().describe('Timestamp en ms del commit resolutor.'),
358
+ }
359
+ )
360
+
361
+ // Checklist de una tarea -- registradas directo con server.registerTool
362
+ // (no con registerCreate/registerUpdate) porque su ruta necesita DOS ids
363
+ // dinámicos (taskId + itemId del checklist), y esos helpers solo arman
364
+ // `${path}/${id}` con uno.
365
+ server.registerTool(
366
+ 'list_checklist_items',
367
+ {
368
+ description: 'Lista los ítems del checklist de una tarea de Orquestra (texto, hecho/pendiente, responsable).',
369
+ inputSchema: {
370
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
371
+ taskId: z.string().describe('ID de la tarea'),
372
+ },
373
+ },
374
+ async ({ projectId, taskId }) => {
375
+ try {
376
+ const data = await apiGet(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId) })
377
+ return textResult(data)
378
+ } catch (err) {
379
+ return errorResult(err)
380
+ }
381
+ }
382
+ )
383
+
384
+ server.registerTool(
385
+ 'add_checklist_item',
386
+ {
387
+ description: 'Agrega un ítem al checklist de una tarea de Orquestra, opcionalmente con un responsable.',
388
+ inputSchema: {
389
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
390
+ taskId: z.string().describe('ID de la tarea'),
391
+ text: z.string().describe('Texto del ítem'),
392
+ assignedTo: z.string().optional().describe('uid del miembro responsable de este ítem (opcional)'),
393
+ },
394
+ },
395
+ async ({ projectId, taskId, ...body }) => {
396
+ try {
397
+ const data = await apiPost(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId), ...body })
398
+ return textResult(data)
399
+ } catch (err) {
400
+ return errorResult(err)
401
+ }
402
+ }
403
+ )
404
+
405
+ server.registerTool(
406
+ 'update_checklist_item',
407
+ {
408
+ description: 'Edita un ítem del checklist de una tarea de Orquestra: texto, hecho/pendiente, o responsable.',
409
+ inputSchema: {
410
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
411
+ taskId: z.string().describe('ID de la tarea'),
412
+ itemId: z.string().describe('ID del ítem del checklist a editar'),
413
+ text: z.string().optional(),
414
+ done: z.boolean().optional(),
415
+ assignedTo: z.string().optional().describe('uid del miembro responsable, o "" para quitarlo'),
416
+ },
417
+ },
418
+ async ({ projectId, taskId, itemId, ...body }) => {
419
+ try {
420
+ const data = await apiPatch(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId), id: itemId, ...body })
421
+ return textResult(data)
422
+ } catch (err) {
423
+ return errorResult(err)
424
+ }
316
425
  }
317
426
  )
318
427
 
@@ -361,6 +470,9 @@ export function createServer() {
361
470
  type: z.enum(DOC_TYPES).describe('Tipo de documento'),
362
471
  title: z.string().describe('Título del documento'),
363
472
  content: z.string().optional(),
473
+ adrStatus: z.enum(ADR_STATUSES).optional().describe('Solo para type=\'adr\'. "aceptado" es lo único que alimenta Material (Contenido) como fuente de ángulos -- ver adrContext.'),
474
+ adrContext: z.string().optional().describe('Solo para type=\'adr\'. Qué problema/situación llevó a la decisión, en texto estructurado (no el markdown libre de content).'),
475
+ adrConsequences: z.string().optional().describe('Solo para type=\'adr\'. Qué cambia de ahora en adelante.'),
364
476
  }
365
477
  )
366
478
 
@@ -376,6 +488,10 @@ export function createServer() {
376
488
  scope: SCOPE_SCHEMA.describe('Alcance por distribución, ej: [{distId, layer, done}]'),
377
489
  tags: z.array(z.string()).optional(),
378
490
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
491
+ shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
492
+ ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
493
+ dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
494
+ closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
379
495
  })
380
496
  registerUpdate('update_module', '/v1/modules', 'Edita un módulo existente (incluido su scope multiplataforma).', {
381
497
  name: z.string().optional(),
@@ -385,6 +501,10 @@ export function createServer() {
385
501
  scope: SCOPE_SCHEMA,
386
502
  tags: z.array(z.string()).optional(),
387
503
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
504
+ shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
505
+ ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
506
+ dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
507
+ closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
388
508
  })
389
509
 
390
510
  registerCreate('create_status', '/v1/statuses', 'Crea un estatus (columna del flujo de tareas) en un proyecto.', {
@@ -459,14 +579,20 @@ export function createServer() {
459
579
  name: z.string().describe('Nombre del servicio'),
460
580
  status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
461
581
  notes: z.string().optional(),
582
+ ownerUid: z.string().optional().describe('uid del miembro responsable del servicio'),
583
+ moduleId: z.string().optional().describe('a qué módulo de producto pertenece este servicio -- ver list_modules'),
584
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este servicio bloquea/afecta -- ver list_tasks'),
462
585
  ...SERVICE_SPECIFIC_SCHEMA,
463
586
  })
464
- registerUpdate('update_service', '/v1/services', 'Edita un servicio de infraestructura existente (status, campos específicos por tipo, incidente...).', {
587
+ registerUpdate('update_service', '/v1/services', 'Edita un servicio de infraestructura existente (status, campos específicos por tipo, incidente...). Un cambio de `status` queda logueado solo en el historial real de incidentes del servicio -- no hace falta nada más para eso. Los demás campos editados quedan logueados en el registro de cambios real del servicio.', {
465
588
  type: z.enum(SERVICE_TYPES).optional(),
466
589
  name: z.string().optional(),
467
590
  status: z.enum(INFRA_STATUSES).optional(),
468
591
  notes: z.string().optional(),
469
592
  lastIncident: z.string().optional(),
593
+ ownerUid: z.string().optional().describe('uid del miembro responsable del servicio'),
594
+ moduleId: z.string().optional().describe('a qué módulo de producto pertenece este servicio -- ver list_modules'),
595
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este servicio bloquea/afecta -- ver list_tasks'),
470
596
  ...SERVICE_SPECIFIC_SCHEMA,
471
597
  })
472
598
 
@@ -478,8 +604,11 @@ export function createServer() {
478
604
  status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
479
605
  notes: z.string().optional(),
480
606
  lastDeploy: z.string().optional().describe('Texto libre, ej. "hace 2 días" -- no es una fecha estructurada'),
607
+ lastDeployAt: z.number().optional().describe('Timestamp en ms del último deploy -- mándalo junto con lastDeployBy al confirmar un deploy real'),
608
+ lastDeployBy: z.string().optional().describe('uid de un miembro, o texto libre tipo "CI" para deploys automáticos'),
609
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este entorno bloquea/afecta -- ver list_tasks'),
481
610
  })
482
- registerUpdate('update_environment', '/v1/environments', 'Edita un entorno existente (útil para actualizar su status/version tras un deploy).', {
611
+ registerUpdate('update_environment', '/v1/environments', 'Edita un entorno existente (útil para actualizar su status/version tras un deploy). Un cambio de `status` queda logueado solo en el historial real de incidentes del entorno.', {
483
612
  name: z.string().optional(),
484
613
  url: z.string().optional(),
485
614
  version: z.string().optional(),
@@ -487,6 +616,9 @@ export function createServer() {
487
616
  status: z.enum(INFRA_STATUSES).optional(),
488
617
  notes: z.string().optional(),
489
618
  lastDeploy: z.string().optional(),
619
+ lastDeployAt: z.number().optional().describe('Timestamp en ms del último deploy'),
620
+ lastDeployBy: z.string().optional().describe('uid de un miembro, o texto libre tipo "CI"'),
621
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este entorno bloquea/afecta -- ver list_tasks'),
490
622
  })
491
623
 
492
624
  registerCreate('create_distribution', '/v1/distributions', 'Crea una distribución/plataforma (iOS, Android, Web, etc.) en un proyecto.', {
@@ -529,6 +661,115 @@ export function createServer() {
529
661
  knowledgeSource: z.string().optional().describe('Doc libre tipo CLAUDE.md para el agente de QA de esta distribución.'),
530
662
  })
531
663
 
664
+ // ═══ Contenido (Fase 1 del rediseño, 11 ago 2026) ═══════════════════════════
665
+ // Un canal = una cuenta real (o a conectar) donde se publica. Una
666
+ // publicación (content) = un canal (channelId), no varios -- para
667
+ // publicar en N canales a la vez, crea N piezas de contenido con
668
+ // create_content, una por channelId.
669
+ const CONTENT_STATUSES = ['idea', 'draft', 'review', 'approved', 'published']
670
+
671
+ registerRead('list_channels', '/v1/channels',
672
+ 'Lista los canales (cuentas de redes/blog/newsletter, reales o a conectar) de un proyecto. Úsala antes de create_channel para no duplicar, y antes de create_content para saber qué channelId usar.',
673
+ {}, (args, projectId) => ({ projectId }))
674
+
675
+ registerCreate('create_channel', '/v1/channels', 'Crea un canal (cuenta de contenido) en un proyecto -- ej. "LinkedIn", "Blog de Atlas". No conecta ninguna cuenta real todavía (eso es OAuth, fuera de alcance por ahora); solo registra el canal para poder programar contenido en él.', {
676
+ name: z.string().describe('Ej. "LinkedIn"'),
677
+ type: z.string().optional().describe('linkedin | x | blog | newsletter | instagram | other -- libre'),
678
+ handle: z.string().optional().describe('Ej. "@atlaspagos" o una URL/dominio'),
679
+ color: z.string().optional(),
680
+ cadenceCount: z.number().optional().describe('Ritmo objetivo de publicaciones, ej. 2'),
681
+ cadenceUnit: z.enum(['week', 'month']).optional(),
682
+ })
683
+
684
+ registerUpdate('update_channel', '/v1/channels', 'Edita un canal existente.', {
685
+ name: z.string().optional(),
686
+ type: z.string().optional(),
687
+ handle: z.string().optional(),
688
+ color: z.string().optional(),
689
+ cadenceCount: z.number().optional(),
690
+ cadenceUnit: z.enum(['week', 'month']).optional(),
691
+ })
692
+
693
+ // Fase 3 de Contenido (11 ago 2026) -- una campaña es una pieza de
694
+ // contenido con plata atrás. budget/spent/leads son casi siempre
695
+ // entrada manual: Orquestra no tiene ninguna integración con
696
+ // plataformas de anuncios para traerlos solos.
697
+ const CAMPAIGN_STATUSES = ['active', 'paused', 'completed']
698
+
699
+ registerRead('list_campaigns', '/v1/campaigns',
700
+ 'Lista las campañas (pauta paga) de un proyecto. Úsala antes de create_campaign para no duplicar.',
701
+ {}, (args, projectId) => ({ projectId }))
702
+
703
+ registerCreate('create_campaign', '/v1/campaigns', 'Crea una campaña de pauta paga en un proyecto.', {
704
+ name: z.string(),
705
+ channelId: z.string().optional().describe('Canal donde corre la campaña -- ver list_channels'),
706
+ budget: z.number().optional().describe('Presupuesto asignado a esta campaña'),
707
+ spent: z.number().optional().describe('Gastado hasta hoy -- entrada manual, no hay integración con la plataforma de ads'),
708
+ leads: z.number().optional().describe('"Registros" generados -- entrada manual'),
709
+ status: z.enum(CAMPAIGN_STATUSES).optional().describe('Default: active. Pasa a paused sola si spent llega al tope (ver update_campaign).'),
710
+ })
711
+
712
+ registerUpdate('update_campaign', '/v1/campaigns', 'Edita una campaña existente -- típicamente para actualizar spent/leads a medida que se revisan en la plataforma de ads real.', {
713
+ name: z.string().optional(),
714
+ channelId: z.string().optional(),
715
+ budget: z.number().optional(),
716
+ spent: z.number().optional(),
717
+ leads: z.number().optional(),
718
+ status: z.enum(CAMPAIGN_STATUSES).optional(),
719
+ })
720
+
721
+ registerRead('list_content', '/v1/content',
722
+ 'Lista las publicaciones de contenido (posts programados/publicados) de un proyecto.',
723
+ {}, (args, projectId) => ({ projectId }))
724
+
725
+ registerCreate('create_content', '/v1/content', 'Crea una publicación de contenido en un canal (ver list_channels/create_channel). Para publicar la misma pieza en varios canales, llama a esto una vez por cada channelId.', {
726
+ channelId: z.string().describe('Canal donde se publica -- ver list_channels'),
727
+ title: z.string().describe('Referencia interna, no es lo que se publica'),
728
+ format: z.string().optional().describe('Ej. "Post", "Reel", "Hilo" -- texto libre según el canal'),
729
+ status: z.enum(CONTENT_STATUSES).optional().describe('Default: idea'),
730
+ caption: z.string().optional().describe('Texto real de la publicación'),
731
+ hashtags: z.array(z.string()).optional(),
732
+ scheduledAt: z.number().optional().describe('Timestamp en ms de cuándo se programa publicar'),
733
+ approvalRequired: z.boolean().optional(),
734
+ approverUid: z.string().optional().describe('uid de quien debe aprobar antes de publicar'),
735
+ changelogVersion: z.string().optional().describe('Versión del changelog que esta publicación anuncia ("Atada a") -- ver list_tasks/create_changelog_entry'),
736
+ moduleId: z.string().optional(),
737
+ ideaId: z.string().optional().describe('Si esta pieza viene de una idea existente'),
738
+ campaignId: z.string().optional().describe('Campaña de pauta a la que pertenece -- ver list_campaigns'),
739
+ organicRate: z.number().optional().describe('% de interacción, medido a mano por el usuario en la plataforma real -- solo tiene sentido con status published'),
740
+ leads: z.number().optional().describe('"Registros" atribuidos a esta pieza orgánica -- entrada manual'),
741
+ reach: z.number().optional().describe('Alcance absoluto -- entrada manual, Fase 4 de Resultados'),
742
+ clicksToSite: z.number().optional().describe('Clics al sitio/docs -- entrada manual'),
743
+ saves: z.number().optional().describe('Guardados -- entrada manual, típico de X/Instagram'),
744
+ avgReadMinutes: z.number().optional().describe('Minutos de lectura media -- entrada manual, típico de blog'),
745
+ language: z.string().optional().describe('\'en\' si esta pieza es la traducción de otra. Sin valor = idioma original.'),
746
+ translationOf: z.string().optional().describe('Si esta pieza es una traducción, id del content original'),
747
+ })
748
+
749
+ registerUpdate('update_content', '/v1/content', 'Edita una publicación de contenido existente. Al pasar status a "approved" o "published" el server estampa quién/cuándo solo -- no lo mandes tú.', {
750
+ channelId: z.string().optional(),
751
+ title: z.string().optional(),
752
+ format: z.string().optional(),
753
+ status: z.enum(CONTENT_STATUSES).optional(),
754
+ caption: z.string().optional(),
755
+ hashtags: z.array(z.string()).optional(),
756
+ scheduledAt: z.number().optional(),
757
+ approvalRequired: z.boolean().optional(),
758
+ approverUid: z.string().optional(),
759
+ changelogVersion: z.string().optional(),
760
+ moduleId: z.string().optional(),
761
+ ideaId: z.string().optional(),
762
+ campaignId: z.string().optional(),
763
+ organicRate: z.number().optional(),
764
+ leads: z.number().optional(),
765
+ reach: z.number().optional(),
766
+ clicksToSite: z.number().optional(),
767
+ saves: z.number().optional(),
768
+ avgReadMinutes: z.number().optional(),
769
+ language: z.string().optional(),
770
+ translationOf: z.string().optional(),
771
+ })
772
+
532
773
  // ═══ QA (módulo de runners -- ver orquestra-qa-agent) ═══════════════════════
533
774
  // Asíncronas: una corrida puede tardar minutos en otra máquina. Ver
534
775
  // SERVER_INSTRUCTIONS para la regla de no asumir "passed" sin confirmar.
@@ -548,9 +789,14 @@ export function createServer() {
548
789
  stack: z.enum(QA_AGENT_STACKS),
549
790
  runnerId: z.string().describe('ID de la máquina emparejada -- ver list_qa_agents o Workspace → QA'),
550
791
  localPath: z.string().describe('Ruta absoluta del checkout en esa máquina'),
551
- branch: z.string().optional().describe('Override de rama para este agente -- default: la del proyecto'),
792
+ branch: z.string().optional().describe('Rama de este agente -- default "main" si no se manda'),
552
793
  distributionId: z.string().optional().describe('Distribución vinculada (repo/fuente de conocimiento)'),
553
794
  enabled: z.boolean().optional(),
795
+ createTasks: z.boolean().optional().describe('Si los fallos de este agente generan tareas automáticas -- default true'),
796
+ openStatusId: z.string().optional().describe('Estatus donde se abren los bugs de este agente -- default: estatus inicial del proyecto'),
797
+ closeStatusId: z.string().optional().describe('Estatus donde se mueven los bugs de este agente al cerrarse solos -- default: primer estatus final'),
798
+ testUserEmail: z.string().optional().describe('Cuenta de prueba DEDICADA (no una credencial real) que la revisión con IA puede usar para iniciar sesión y revisar pantallas autenticadas'),
799
+ testUserPassword: z.string().optional().describe('Password de testUserEmail'),
554
800
  })
555
801
 
556
802
  registerUpdate('update_qa_agent', '/v1/qaAgents', 'Edita un agente de QA existente.', {
@@ -561,11 +807,16 @@ export function createServer() {
561
807
  branch: z.string().optional(),
562
808
  distributionId: z.string().optional(),
563
809
  enabled: z.boolean().optional(),
810
+ createTasks: z.boolean().optional(),
811
+ openStatusId: z.string().optional(),
812
+ closeStatusId: z.string().optional(),
813
+ testUserEmail: z.string().optional().describe('Cuenta de prueba DEDICADA (no una credencial real) para que la revisión con IA inicie sesión'),
814
+ testUserPassword: z.string().optional(),
564
815
  })
565
816
 
566
- registerCreate('create_qa_run', '/v1/qa/runs', 'Dispara una corrida de QA (lint/tests/build/E2E según el stack) en la rama indicada. El proyecto necesita QA activado y al menos un agente de QA para el stack (o falla explícito -- ver list_qa_agents/create_qa_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_qa_run.', {
817
+ registerCreate('create_qa_run', '/v1/qa/runs', 'Dispara una corrida de QA (lint/tests/build/E2E según el stack) en la rama indicada. Necesita al menos un agente de QA activo para el stack (o falla explícito -- ver list_qa_agents/create_qa_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_qa_run.', {
567
818
  agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack'),
568
- branch: z.string().optional().describe('Rama a probar -- default: la del agente, o la del proyecto, o "main"'),
819
+ branch: z.string().optional().describe('Rama a probar -- default: la del agente, o "main"'),
569
820
  commit: z.string().optional(),
570
821
  trigger: z.enum(['manual', 'push', 'schedule']).optional().describe('Default: manual'),
571
822
  stack: z.string().optional().describe('Stack a probar (ej. "web", "ios", "android") -- resuelve contra los agentes activos de ese stack. Falla si hay 0 (créalo con create_qa_agent) o más de 1 (usa agentId).'),
package/src/setup.mjs CHANGED
@@ -75,9 +75,13 @@ export async function runSetup() {
75
75
  const idToken = await idTokenPromise
76
76
  console.log('Login recibido, configurando...')
77
77
 
78
+ const agentLabelAnswer = await prompt('¿Cómo se llama este agente/herramienta? [Claude Code]: ')
79
+ const agentLabel = agentLabelAnswer.trim() || 'Claude Code'
80
+
78
81
  const res = await fetch(`${apiBase}/v1/bootstrap`, {
79
82
  method: 'POST',
80
- headers: { Authorization: `Bearer ${idToken}` },
83
+ headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({ agentLabel }),
81
85
  })
82
86
  const body = await res.json()
83
87
  if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)