orquestra-mcp 1.6.4 → 1.7.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,7 +1,7 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.6.4",
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.0",
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,10 +300,24 @@ 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
+ 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.'),
308
+ 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.'),
309
+ estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
310
+ stack: z.array(z.string()).optional().describe('Tecnologías involucradas (mismo vocabulario que el stack de una distribución, ej. "node", "react", "postgres").'),
311
+ branch: z.string().optional().describe('Branch de código asociado a esta tarea.'),
312
+ prNumber: z.string().optional().describe('Número del Pull Request asociado (sin el "#").'),
313
+ prUrl: z.string().optional(),
314
+ prStatus: z.string().optional().describe('"open" | "merged" | "closed", o lo que corresponda -- texto libre.'),
315
+ commitCount: z.number().optional(),
316
+ lastCommitAt: z.number().optional().describe('Timestamp en ms del último commit conocido.'),
317
+ 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).'),
318
+ resolvedCommitMessage: z.string().optional(),
319
+ resolvedCommitUrl: z.string().optional(),
320
+ resolvedCommitAt: z.number().optional().describe('Timestamp en ms del commit resolutor.'),
291
321
  }
292
322
  )
293
323
 
@@ -308,7 +338,88 @@ export function createServer() {
308
338
  tags: z.array(z.string()).optional(),
309
339
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
310
340
  .describe('Alcance por distribución -- usa get_parity_summary para ver el scope actual antes de editarlo'),
341
+ relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. Ver create_task.'),
342
+ location: z.string().optional().describe('Solo relevante para type "meeting"/"call"/"followup"/"business". Ver create_task.'),
311
343
  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é.'),
344
+ estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
345
+ stack: z.array(z.string()).optional().describe('Tecnologías involucradas -- ver create_task.'),
346
+ 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.'),
347
+ prNumber: z.string().optional().describe('Número del Pull Request asociado (sin el "#").'),
348
+ prUrl: z.string().optional(),
349
+ prStatus: z.string().optional().describe('"open" | "merged" | "closed", o lo que corresponda -- texto libre.'),
350
+ commitCount: z.number().optional(),
351
+ lastCommitAt: z.number().optional().describe('Timestamp en ms del último commit conocido.'),
352
+ 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).'),
353
+ resolvedCommitMessage: z.string().optional(),
354
+ resolvedCommitUrl: z.string().optional(),
355
+ resolvedCommitAt: z.number().optional().describe('Timestamp en ms del commit resolutor.'),
356
+ }
357
+ )
358
+
359
+ // Checklist de una tarea -- registradas directo con server.registerTool
360
+ // (no con registerCreate/registerUpdate) porque su ruta necesita DOS ids
361
+ // dinámicos (taskId + itemId del checklist), y esos helpers solo arman
362
+ // `${path}/${id}` con uno.
363
+ server.registerTool(
364
+ 'list_checklist_items',
365
+ {
366
+ description: 'Lista los ítems del checklist de una tarea de Orquestra (texto, hecho/pendiente, responsable).',
367
+ inputSchema: {
368
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
369
+ taskId: z.string().describe('ID de la tarea'),
370
+ },
371
+ },
372
+ async ({ projectId, taskId }) => {
373
+ try {
374
+ const data = await apiGet(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId) })
375
+ return textResult(data)
376
+ } catch (err) {
377
+ return errorResult(err)
378
+ }
379
+ }
380
+ )
381
+
382
+ server.registerTool(
383
+ 'add_checklist_item',
384
+ {
385
+ description: 'Agrega un ítem al checklist de una tarea de Orquestra, opcionalmente con un responsable.',
386
+ inputSchema: {
387
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
388
+ taskId: z.string().describe('ID de la tarea'),
389
+ text: z.string().describe('Texto del ítem'),
390
+ assignedTo: z.string().optional().describe('uid del miembro responsable de este ítem (opcional)'),
391
+ },
392
+ },
393
+ async ({ projectId, taskId, ...body }) => {
394
+ try {
395
+ const data = await apiPost(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId), ...body })
396
+ return textResult(data)
397
+ } catch (err) {
398
+ return errorResult(err)
399
+ }
400
+ }
401
+ )
402
+
403
+ server.registerTool(
404
+ 'update_checklist_item',
405
+ {
406
+ description: 'Edita un ítem del checklist de una tarea de Orquestra: texto, hecho/pendiente, o responsable.',
407
+ inputSchema: {
408
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
409
+ taskId: z.string().describe('ID de la tarea'),
410
+ itemId: z.string().describe('ID del ítem del checklist a editar'),
411
+ text: z.string().optional(),
412
+ done: z.boolean().optional(),
413
+ assignedTo: z.string().optional().describe('uid del miembro responsable, o "" para quitarlo'),
414
+ },
415
+ },
416
+ async ({ projectId, taskId, itemId, ...body }) => {
417
+ try {
418
+ const data = await apiPatch(config, `/v1/tasks/${taskId}/checklist`, { projectId: resolveProjectId(projectId), id: itemId, ...body })
419
+ return textResult(data)
420
+ } catch (err) {
421
+ return errorResult(err)
422
+ }
312
423
  }
313
424
  )
314
425
 
@@ -357,6 +468,9 @@ export function createServer() {
357
468
  type: z.enum(DOC_TYPES).describe('Tipo de documento'),
358
469
  title: z.string().describe('Título del documento'),
359
470
  content: z.string().optional(),
471
+ 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.'),
472
+ 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).'),
473
+ adrConsequences: z.string().optional().describe('Solo para type=\'adr\'. Qué cambia de ahora en adelante.'),
360
474
  }
361
475
  )
362
476
 
@@ -372,6 +486,10 @@ export function createServer() {
372
486
  scope: SCOPE_SCHEMA.describe('Alcance por distribución, ej: [{distId, layer, done}]'),
373
487
  tags: z.array(z.string()).optional(),
374
488
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
489
+ shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
490
+ ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
491
+ dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
492
+ closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
375
493
  })
376
494
  registerUpdate('update_module', '/v1/modules', 'Edita un módulo existente (incluido su scope multiplataforma).', {
377
495
  name: z.string().optional(),
@@ -381,6 +499,10 @@ export function createServer() {
381
499
  scope: SCOPE_SCHEMA,
382
500
  tags: z.array(z.string()).optional(),
383
501
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
502
+ shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
503
+ ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
504
+ dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
505
+ closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
384
506
  })
385
507
 
386
508
  registerCreate('create_status', '/v1/statuses', 'Crea un estatus (columna del flujo de tareas) en un proyecto.', {
@@ -455,14 +577,20 @@ export function createServer() {
455
577
  name: z.string().describe('Nombre del servicio'),
456
578
  status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
457
579
  notes: z.string().optional(),
580
+ ownerUid: z.string().optional().describe('uid del miembro responsable del servicio'),
581
+ moduleId: z.string().optional().describe('a qué módulo de producto pertenece este servicio -- ver list_modules'),
582
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este servicio bloquea/afecta -- ver list_tasks'),
458
583
  ...SERVICE_SPECIFIC_SCHEMA,
459
584
  })
460
- registerUpdate('update_service', '/v1/services', 'Edita un servicio de infraestructura existente (status, campos específicos por tipo, incidente...).', {
585
+ 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.', {
461
586
  type: z.enum(SERVICE_TYPES).optional(),
462
587
  name: z.string().optional(),
463
588
  status: z.enum(INFRA_STATUSES).optional(),
464
589
  notes: z.string().optional(),
465
590
  lastIncident: z.string().optional(),
591
+ ownerUid: z.string().optional().describe('uid del miembro responsable del servicio'),
592
+ moduleId: z.string().optional().describe('a qué módulo de producto pertenece este servicio -- ver list_modules'),
593
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este servicio bloquea/afecta -- ver list_tasks'),
466
594
  ...SERVICE_SPECIFIC_SCHEMA,
467
595
  })
468
596
 
@@ -474,8 +602,11 @@ export function createServer() {
474
602
  status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
475
603
  notes: z.string().optional(),
476
604
  lastDeploy: z.string().optional().describe('Texto libre, ej. "hace 2 días" -- no es una fecha estructurada'),
605
+ lastDeployAt: z.number().optional().describe('Timestamp en ms del último deploy -- mándalo junto con lastDeployBy al confirmar un deploy real'),
606
+ lastDeployBy: z.string().optional().describe('uid de un miembro, o texto libre tipo "CI" para deploys automáticos'),
607
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este entorno bloquea/afecta -- ver list_tasks'),
477
608
  })
478
- registerUpdate('update_environment', '/v1/environments', 'Edita un entorno existente (útil para actualizar su status/version tras un deploy).', {
609
+ 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.', {
479
610
  name: z.string().optional(),
480
611
  url: z.string().optional(),
481
612
  version: z.string().optional(),
@@ -483,6 +614,9 @@ export function createServer() {
483
614
  status: z.enum(INFRA_STATUSES).optional(),
484
615
  notes: z.string().optional(),
485
616
  lastDeploy: z.string().optional(),
617
+ lastDeployAt: z.number().optional().describe('Timestamp en ms del último deploy'),
618
+ lastDeployBy: z.string().optional().describe('uid de un miembro, o texto libre tipo "CI"'),
619
+ affectedTaskIds: z.array(z.string()).optional().describe('IDs de tareas que este entorno bloquea/afecta -- ver list_tasks'),
486
620
  })
487
621
 
488
622
  registerCreate('create_distribution', '/v1/distributions', 'Crea una distribución/plataforma (iOS, Android, Web, etc.) en un proyecto.', {
@@ -525,6 +659,115 @@ export function createServer() {
525
659
  knowledgeSource: z.string().optional().describe('Doc libre tipo CLAUDE.md para el agente de QA de esta distribución.'),
526
660
  })
527
661
 
662
+ // ═══ Contenido (Fase 1 del rediseño, 11 ago 2026) ═══════════════════════════
663
+ // Un canal = una cuenta real (o a conectar) donde se publica. Una
664
+ // publicación (content) = un canal (channelId), no varios -- para
665
+ // publicar en N canales a la vez, crea N piezas de contenido con
666
+ // create_content, una por channelId.
667
+ const CONTENT_STATUSES = ['idea', 'draft', 'review', 'approved', 'published']
668
+
669
+ registerRead('list_channels', '/v1/channels',
670
+ '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.',
671
+ {}, (args, projectId) => ({ projectId }))
672
+
673
+ 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.', {
674
+ name: z.string().describe('Ej. "LinkedIn"'),
675
+ type: z.string().optional().describe('linkedin | x | blog | newsletter | instagram | other -- libre'),
676
+ handle: z.string().optional().describe('Ej. "@atlaspagos" o una URL/dominio'),
677
+ color: z.string().optional(),
678
+ cadenceCount: z.number().optional().describe('Ritmo objetivo de publicaciones, ej. 2'),
679
+ cadenceUnit: z.enum(['week', 'month']).optional(),
680
+ })
681
+
682
+ registerUpdate('update_channel', '/v1/channels', 'Edita un canal existente.', {
683
+ name: z.string().optional(),
684
+ type: z.string().optional(),
685
+ handle: z.string().optional(),
686
+ color: z.string().optional(),
687
+ cadenceCount: z.number().optional(),
688
+ cadenceUnit: z.enum(['week', 'month']).optional(),
689
+ })
690
+
691
+ // Fase 3 de Contenido (11 ago 2026) -- una campaña es una pieza de
692
+ // contenido con plata atrás. budget/spent/leads son casi siempre
693
+ // entrada manual: Orquestra no tiene ninguna integración con
694
+ // plataformas de anuncios para traerlos solos.
695
+ const CAMPAIGN_STATUSES = ['active', 'paused', 'completed']
696
+
697
+ registerRead('list_campaigns', '/v1/campaigns',
698
+ 'Lista las campañas (pauta paga) de un proyecto. Úsala antes de create_campaign para no duplicar.',
699
+ {}, (args, projectId) => ({ projectId }))
700
+
701
+ registerCreate('create_campaign', '/v1/campaigns', 'Crea una campaña de pauta paga en un proyecto.', {
702
+ name: z.string(),
703
+ channelId: z.string().optional().describe('Canal donde corre la campaña -- ver list_channels'),
704
+ budget: z.number().optional().describe('Presupuesto asignado a esta campaña'),
705
+ spent: z.number().optional().describe('Gastado hasta hoy -- entrada manual, no hay integración con la plataforma de ads'),
706
+ leads: z.number().optional().describe('"Registros" generados -- entrada manual'),
707
+ status: z.enum(CAMPAIGN_STATUSES).optional().describe('Default: active. Pasa a paused sola si spent llega al tope (ver update_campaign).'),
708
+ })
709
+
710
+ 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.', {
711
+ name: z.string().optional(),
712
+ channelId: z.string().optional(),
713
+ budget: z.number().optional(),
714
+ spent: z.number().optional(),
715
+ leads: z.number().optional(),
716
+ status: z.enum(CAMPAIGN_STATUSES).optional(),
717
+ })
718
+
719
+ registerRead('list_content', '/v1/content',
720
+ 'Lista las publicaciones de contenido (posts programados/publicados) de un proyecto.',
721
+ {}, (args, projectId) => ({ projectId }))
722
+
723
+ 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.', {
724
+ channelId: z.string().describe('Canal donde se publica -- ver list_channels'),
725
+ title: z.string().describe('Referencia interna, no es lo que se publica'),
726
+ format: z.string().optional().describe('Ej. "Post", "Reel", "Hilo" -- texto libre según el canal'),
727
+ status: z.enum(CONTENT_STATUSES).optional().describe('Default: idea'),
728
+ caption: z.string().optional().describe('Texto real de la publicación'),
729
+ hashtags: z.array(z.string()).optional(),
730
+ scheduledAt: z.number().optional().describe('Timestamp en ms de cuándo se programa publicar'),
731
+ approvalRequired: z.boolean().optional(),
732
+ approverUid: z.string().optional().describe('uid de quien debe aprobar antes de publicar'),
733
+ changelogVersion: z.string().optional().describe('Versión del changelog que esta publicación anuncia ("Atada a") -- ver list_tasks/create_changelog_entry'),
734
+ moduleId: z.string().optional(),
735
+ ideaId: z.string().optional().describe('Si esta pieza viene de una idea existente'),
736
+ campaignId: z.string().optional().describe('Campaña de pauta a la que pertenece -- ver list_campaigns'),
737
+ organicRate: z.number().optional().describe('% de interacción, medido a mano por el usuario en la plataforma real -- solo tiene sentido con status published'),
738
+ leads: z.number().optional().describe('"Registros" atribuidos a esta pieza orgánica -- entrada manual'),
739
+ reach: z.number().optional().describe('Alcance absoluto -- entrada manual, Fase 4 de Resultados'),
740
+ clicksToSite: z.number().optional().describe('Clics al sitio/docs -- entrada manual'),
741
+ saves: z.number().optional().describe('Guardados -- entrada manual, típico de X/Instagram'),
742
+ avgReadMinutes: z.number().optional().describe('Minutos de lectura media -- entrada manual, típico de blog'),
743
+ language: z.string().optional().describe('\'en\' si esta pieza es la traducción de otra. Sin valor = idioma original.'),
744
+ translationOf: z.string().optional().describe('Si esta pieza es una traducción, id del content original'),
745
+ })
746
+
747
+ 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ú.', {
748
+ channelId: z.string().optional(),
749
+ title: z.string().optional(),
750
+ format: z.string().optional(),
751
+ status: z.enum(CONTENT_STATUSES).optional(),
752
+ caption: z.string().optional(),
753
+ hashtags: z.array(z.string()).optional(),
754
+ scheduledAt: z.number().optional(),
755
+ approvalRequired: z.boolean().optional(),
756
+ approverUid: z.string().optional(),
757
+ changelogVersion: z.string().optional(),
758
+ moduleId: z.string().optional(),
759
+ ideaId: z.string().optional(),
760
+ campaignId: z.string().optional(),
761
+ organicRate: z.number().optional(),
762
+ leads: z.number().optional(),
763
+ reach: z.number().optional(),
764
+ clicksToSite: z.number().optional(),
765
+ saves: z.number().optional(),
766
+ avgReadMinutes: z.number().optional(),
767
+ language: z.string().optional(),
768
+ translationOf: z.string().optional(),
769
+ })
770
+
528
771
  // ═══ QA (módulo de runners -- ver orquestra-qa-agent) ═══════════════════════
529
772
  // Asíncronas: una corrida puede tardar minutos en otra máquina. Ver
530
773
  // SERVER_INSTRUCTIONS para la regla de no asumir "passed" sin confirmar.
@@ -544,9 +787,14 @@ export function createServer() {
544
787
  stack: z.enum(QA_AGENT_STACKS),
545
788
  runnerId: z.string().describe('ID de la máquina emparejada -- ver list_qa_agents o Workspace → QA'),
546
789
  localPath: z.string().describe('Ruta absoluta del checkout en esa máquina'),
547
- branch: z.string().optional().describe('Override de rama para este agente -- default: la del proyecto'),
790
+ branch: z.string().optional().describe('Rama de este agente -- default "main" si no se manda'),
548
791
  distributionId: z.string().optional().describe('Distribución vinculada (repo/fuente de conocimiento)'),
549
792
  enabled: z.boolean().optional(),
793
+ createTasks: z.boolean().optional().describe('Si los fallos de este agente generan tareas automáticas -- default true'),
794
+ openStatusId: z.string().optional().describe('Estatus donde se abren los bugs de este agente -- default: estatus inicial del proyecto'),
795
+ closeStatusId: z.string().optional().describe('Estatus donde se mueven los bugs de este agente al cerrarse solos -- default: primer estatus final'),
796
+ 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'),
797
+ testUserPassword: z.string().optional().describe('Password de testUserEmail'),
550
798
  })
551
799
 
552
800
  registerUpdate('update_qa_agent', '/v1/qaAgents', 'Edita un agente de QA existente.', {
@@ -557,11 +805,16 @@ export function createServer() {
557
805
  branch: z.string().optional(),
558
806
  distributionId: z.string().optional(),
559
807
  enabled: z.boolean().optional(),
808
+ createTasks: z.boolean().optional(),
809
+ openStatusId: z.string().optional(),
810
+ closeStatusId: z.string().optional(),
811
+ testUserEmail: z.string().optional().describe('Cuenta de prueba DEDICADA (no una credencial real) para que la revisión con IA inicie sesión'),
812
+ testUserPassword: z.string().optional(),
560
813
  })
561
814
 
562
- 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.', {
815
+ 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.', {
563
816
  agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack'),
564
- branch: z.string().optional().describe('Rama a probar -- default: la del agente, o la del proyecto, o "main"'),
817
+ branch: z.string().optional().describe('Rama a probar -- default: la del agente, o "main"'),
565
818
  commit: z.string().optional(),
566
819
  trigger: z.enum(['manual', 'push', 'schedule']).optional().describe('Default: manual'),
567
820
  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}`)