orquestra-mcp 1.16.1 → 1.17.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # orquestra-mcp
2
2
 
3
- Servidor MCP (Model Context Protocol) para Orquestra. Expone **38 tools**
3
+ Servidor MCP (Model Context Protocol) para Orquestra. Expone **89 tools**
4
4
  que llaman a la API de `orquestra-infra`: lectura de contexto/dashboard/
5
5
  workspace/infra/paridad/listados, escritura de tareas/ideas/changelog/
6
6
  notas/docs/módulos/flujo/categorías/infra/proyectos, y corridas de QA —
@@ -192,11 +192,13 @@ create/update sigue siendo criterio del agente y del humano.
192
192
 
193
193
  ## Estado
194
194
 
195
- 38 tools: 9 de lectura (contexto/tasks/search + dashboard/workspace/infra/
196
- paridad/setup guide/stage playbook) + 6 `list_*` + 19 de creación/edición
197
- (tasks/ideas/changelog/notes/docs + módulos/flujo/categorías/servicios/
198
- entornos/distribuciones/proyecto) + 2 de Agentes (create_agent_run/get_agent_run) +
199
- 2 compuestas (init_project, apply_stage_playbook). No hay `delete_*`, ni
200
- `update_idea`/`update_note`/`update_doc`/`update_changelog_entry`
201
- todavía. Publicado en npm como `orquestra-mcp` (unscoped) -- `npx
202
- orquestra-mcp setup`.
195
+ 89 tools: lectura (contexto, dashboard, workspace, infra, paridad, standup,
196
+ sync report, setup guide, playbooks de etapa y de plataforma, catálogo de
197
+ skills, search) + `list_*` de cada colección + creación/edición de tareas
198
+ (con checklist, tiempo, adjuntos, merge), ideas (con comentarios), notas,
199
+ docs, changelog (con corte de versión), módulos, flujo, categorías,
200
+ infraestructura, proyecto, contenido (canales, campañas, contenido,
201
+ Postproxy), agentes, skills y sugerencias + compuestas (init_project,
202
+ apply_stage_playbook, apply_platform_playbook, install_skill). Sin
203
+ `delete_*` salvo adjuntos e ítems de checklist. Publicado en npm como
204
+ `orquestra-mcp` (unscoped) -- `npx orquestra-mcp setup`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "Servidor MCP de Orquestra -- 89 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": {
@@ -102,7 +102,7 @@ export const PLATFORM_PLAYBOOKS = {
102
102
  {
103
103
  title: 'Preparar capturas de pantalla en los tamaños exactos que pide App Store Connect',
104
104
  description: 'Las dimensiones válidas cambian con cada generación de iPhone/iPad -- si App Store Connect rechaza una captura al subirla, el mensaje de error indica el tamaño exacto que espera para ese dispositivo; ajusta según ese mensaje real, no de memoria.',
105
- type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 7,
105
+ type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 7, checklistTool: 'screenshots',
106
106
  },
107
107
  {
108
108
  title: 'Completar el cuestionario de privacidad de la tienda (Data Collection)',
@@ -251,7 +251,7 @@ export const PLATFORM_PLAYBOOKS = {
251
251
  {
252
252
  title: 'Preparar capturas de pantalla en los tamaños exactos que pide Play Console',
253
253
  description: 'Mínimo 2 capturas de teléfono, más el gráfico destacado (1024x500) y el ícono (512x512) -- Play Console valida las dimensiones al subir, ajusta según lo que rechace.',
254
- type: 'business', priority: 'medium', tags: ['publicacion', 'android'], offsetDays: 7,
254
+ type: 'business', priority: 'medium', tags: ['publicacion', 'android'], offsetDays: 7, checklistTool: 'screenshots',
255
255
  },
256
256
  {
257
257
  title: 'Completar el formulario de seguridad de los datos (Data safety)',
package/src/server.mjs CHANGED
@@ -10,6 +10,10 @@ import { PLATFORM_PLAYBOOKS } from './platformPlaybooks.mjs'
10
10
  import { SKILLS_CATALOG } from './skillsCatalog.mjs'
11
11
  import { parseSource, previewSkill, installSkill } from './skillInstall.mjs'
12
12
 
13
+ // Versión leída de package.json (3 sep 2026) -- antes estaba hardcodeada
14
+ // acá y quedaba atrás en cada bump (decía 1.16.0 con package.json en 1.16.2).
15
+ const PKG_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
16
+
13
17
  function errorResult(err) {
14
18
  const text = err instanceof ApiError ? `Error (${err.status}): ${err.message}` : `Error: ${err.message}`
15
19
  return { content: [{ type: 'text', text }], isError: true }
@@ -93,6 +97,7 @@ Durante el trabajo normal de código, sin que el usuario te lo pida explícitame
93
97
  - Si una nota que vas a crear (create_note) es claramente sobre un módulo o una tarea específica -- por ejemplo durante un setup guiado donde acabas de crear ambos -- mándale moduleId/taskId para ligarla, en vez de dejarla suelta. No inventes la relación si no es clara.
94
98
  - create_changelog_entry SIEMPRE deja la entrada en Unreleased (nunca mandes version salvo que el usuario te haya dado un número real) -- cortar una versión es release_changelog_version, una acción DELIBERADA que solo se llama si el usuario la pide explícitamente ("cortá la versión", "hacé el release de X") o confirma un número/fecha real que vos propusiste. No la dispares solo porque terminaste una tarea o un lote de trabajo grande.
95
99
  - 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.
100
+ - Paridad por producto: las distribuciones que son el mismo producto en distintas plataformas (típicamente Web/iOS/Android) comparten un productKey (list_distributions lo muestra). Si al crear una tarea con scope en una de ellas la respuesta trae parityAddedDistIds, el server sumó las hermanas como pendientes -- dícelo al usuario ("también quedó pendiente en iOS y Android") y no lo deshagas salvo que el trabajo sea genuinamente de una sola plataforma (entonces skipParity:true, o quítalas con update_task/scope explicando por qué). Al cerrar una tarea con scope multiplataforma, marca done:true SOLO en la plataforma donde de verdad se hizo (update_task/scope) -- el estatus "Listo" no mueve esos flags solo; dejar las hermanas en done:false es exactamente lo que hace que el desfase aparezca en get_parity_summary y en la revisión de sincronización (tasksClosedWithParityPending), que es el punto: que nunca se olvide el trabajo del otro lado. Si el proyecto tiene 2+ plataformas y ninguna tiene productKey (get_parity_summary → productGroups.configured=false), pregúntale al usuario cuáles son el mismo producto y configúralo con update_distribution -- nunca lo infieras solo del tipo de plataforma.
96
101
  - Al crear o editar una tarea (o un módulo), llena TODO campo para el que tengas un dato real, no solo title/moduleId/dueDate/comment: type (no lo dejes en el default "feature" si en realidad es un bug/mejora/doc/etc.), tags, stack (tecnologías que genuinamente tocaste), scope (paridad multiplataforma, si aplica), y los campos de código (branch/prNumber/prUrl/prStatus/commitCount/resolvedCommitSha/resolvedCommitMessage/resolvedCommitUrl/resolvedCommitAt) cuando de verdad sepas esos valores (el branch en el que estás trabajando, el PR que abriste, el commit que resolvió la tarea). No dejes un campo en blanco solo porque no es obligatorio -- pero esto no es licencia para inventar: si no tenés el dato real (ej. no hay branch/PR porque el proyecto commitea directo a main), dejalo vacío en vez de rellenarlo con algo plausible pero falso -- mismo criterio que ya se pedía para resolvedCommitSha.
97
102
  - Si una tarea tiene más de una actividad o paso concreto, no los amontones como texto corrido en description -- usa el checklist (add_checklist_item, un ítem por actividad, opcionalmente con responsable) para que cada paso se pueda marcar independientemente. description queda para el contexto/propósito general de la tarea (el qué y el por qué), no como lista de pasos. Si estás editando una tarea existente cuya description ya creció así, migra esos puntos a checklist en vez de seguir agregando ahí.
98
103
  - Al cerrar una tarea en la que trabajaste de verdad (o al final de una sesión de trabajo real sobre ella), registrá las horas con log_time -- el tiempo REAL que llevó ese trabajo en esta sesión (aproximalo con honestidad: minutos de trabajo real, no la duración del chat ni la estimación de la tarea), con una nota corta de qué se hizo. Ese registro alimenta "Horas del equipo" en Inversión de Contenido y es la única forma de que el costo real del proyecto no dependa de que el usuario lo cargue a mano. No lo inventes ni lo infles; si genuinamente no podés estimar el tiempo de la sesión, no registres nada.
@@ -103,6 +108,7 @@ Si el usuario pide "sincronizar"/"revisar que todo esté al día"/"verificar el
103
108
  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).
104
109
  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.
105
110
  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.
111
+ 4b. Paridad (solo si productGroups.configured): tasksClosedWithParityPending son tareas ya cerradas con trabajo pendiente en una plataforma hermana (pendingDistNames). Para cada una decidí con el usuario: si en realidad YA se hizo ahí, marcá done:true en ese scope con update_task; si no se hizo, creá la tarea de esa plataforma (mismo módulo, relatedTaskId opcional) -- nunca marques done sin evidencia real. tasksMissingSiblingScope son tareas activas que tocan una hermana sin declarar a las demás: confirmá si aplica y extendé el scope, o dejalo si es trabajo de una sola plataforma.
106
112
  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.
107
113
 
108
114
  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.
@@ -115,7 +121,7 @@ Si el usuario pregunta explícito qué Claude Skills de terceros le faltan (o pi
115
121
 
116
122
  export function createServer() {
117
123
  const config = loadConfig()
118
- const server = new McpServer({ name: 'orquestra-mcp', version: '1.16.0' }, { instructions: SERVER_INSTRUCTIONS })
124
+ const server = new McpServer({ name: 'orquestra-mcp', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS })
119
125
 
120
126
  function resolveProjectId(projectId) {
121
127
  if (config.defaultProjectId) return config.defaultProjectId
@@ -246,7 +252,7 @@ export function createServer() {
246
252
  registerRead(
247
253
  'get_parity_summary',
248
254
  '/v1/parity',
249
- 'Desfases de paridad multiplataforma: tareas y módulos cuyo scope avanza en una distribución pero sigue pendiente en otra.',
255
+ 'Desfases de paridad multiplataforma: tareas y módulos cuyo scope avanza en una distribución pero sigue pendiente en otra HERMANA (mismo productKey -- ver productGroups en la respuesta; si configured=false, el proyecto nunca marcó qué plataformas son el mismo producto y todas se comparan entre sí: sugiérele al usuario configurarlo con update_distribution/productKey). singleDoneItems trae las candidatas a portar con missingDistIds concretos.',
250
256
  {},
251
257
  (args, projectId) => ({ projectId })
252
258
  )
@@ -505,7 +511,8 @@ export function createServer() {
505
511
  assignedTo: z.string().optional().describe('uid del miembro asignado'),
506
512
  tags: z.array(z.string()).optional(),
507
513
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
508
- .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.'),
514
+ .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. Si una distId tiene hermanas (mismo productKey), el server suma las que falten solo, con done:false -- la respuesta trae parityAddedDistIds con lo que agregó: avísale al usuario que la tarea también quedó pendiente en esas plataformas.'),
515
+ 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.'),
509
516
  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.'),
510
517
  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.'),
511
518
  estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
@@ -613,10 +620,10 @@ export function createServer() {
613
620
  server.registerTool(
614
621
  'connect_postproxy_channel',
615
622
  {
616
- description: 'Inicia la conexión de un canal (facebook/instagram/tiktok) a Postproxy. Devuelve UNA de dos formas: {url} -- OAuth hosteado por Postproxy, el usuario tiene que abrirlo y completarlo a mano (el agente no puede terminar un OAuth por su cuenta), pásale esa URL tal cual; o {needsPageSelection, profileId, pages} -- solo pasa con facebook, cuando ya hay un login de Facebook conectado en la cuenta (una sola cuenta da acceso a varias Pages, no hace falta repetir el OAuth) y hace falta elegir cuál Page corresponde a este canal. Esa elección hoy es un flujo de la web (Cuentas conectadas) -- si te llega `needsPageSelection`, dile al usuario que complete la selección ahí, esta tool no tiene una contraparte para elegir la página. No confundir con connect_channel_to_buttondown -- esto es solo para los tipos que soporta Postproxy.',
623
+ description: 'Inicia la conexión de un canal (facebook/instagram/tiktok/linkedin) a Postproxy. Devuelve UNA de dos formas: {url} -- OAuth hosteado por Postproxy, el usuario tiene que abrirlo y completarlo a mano (el agente no puede terminar un OAuth por su cuenta), pásale esa URL tal cual; o {needsPageSelection, profileId, pages} -- pasa con facebook y linkedin, cuando ya hay un login de esa red conectado en la cuenta (un solo login da acceso a varias Pages de Facebook, o al perfil personal y las Organization Pages de LinkedIn, no hace falta repetir el OAuth) y hace falta elegir cuál corresponde a este canal. Esa elección hoy es un flujo de la web (Cuentas conectadas) -- si te llega `needsPageSelection`, dile al usuario que complete la selección ahí, esta tool no tiene una contraparte para elegir la página. Solo para los tipos que soporta Postproxy -- un canal newsletter (Buttondown) se conecta desde la web, no con esta tool.',
617
624
  inputSchema: {
618
625
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
619
- channelId: z.string().describe('Canal a conectar (ver list_channels) -- su `type` tiene que ser facebook/instagram/tiktok'),
626
+ channelId: z.string().describe('Canal a conectar (ver list_channels) -- su `type` tiene que ser facebook/instagram/tiktok/linkedin'),
620
627
  platform: z.string().describe('Plataforma tal cual la espera Postproxy (mismo valor que channel.type -- ej. "instagram")'),
621
628
  },
622
629
  },
@@ -1443,6 +1450,7 @@ export function createServer() {
1443
1450
  label: z.string().describe('Nombre a mostrar por default'),
1444
1451
  name: z.string().optional().describe('Nombre editable -- si no se manda, la UI usa label'),
1445
1452
  categoryId: z.string().optional(),
1453
+ productKey: z.string().optional().describe('Paridad por producto: clave libre (ej. "app") que agrupa las distribuciones que son EL MISMO producto en distintas plataformas (Web/iOS/Android). Hermanas = misma clave. Con eso la paridad solo compara entre hermanas y create_task propone el scope de las demás solo. Landing/backend/MCP normalmente van sin clave. Solo asígnala si el usuario confirma qué plataformas son el mismo producto -- no la infieras del tipo.'),
1446
1454
  status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
1447
1455
  phase: z.string().optional().describe('idea | design | alpha | beta | live | deprecated'),
1448
1456
  identifier: z.string().optional().describe('Legado -- preferir bundleId (no-web) o domain (web)'),
@@ -1463,6 +1471,7 @@ export function createServer() {
1463
1471
  label: z.string().optional(),
1464
1472
  name: z.string().optional(),
1465
1473
  categoryId: z.string().optional(),
1474
+ productKey: z.string().optional().describe('Paridad por producto -- misma clave en las distribuciones que son el mismo producto (ver create_distribution). "" la quita.'),
1466
1475
  status: z.enum(INFRA_STATUSES).optional(),
1467
1476
  phase: z.string().optional(),
1468
1477
  identifier: z.string().optional(),
@@ -1518,7 +1527,7 @@ export function createServer() {
1518
1527
 
1519
1528
  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.', {
1520
1529
  name: z.string().describe('Ej. "LinkedIn"'),
1521
- type: z.string().optional().describe('linkedin | x | blog | newsletter | instagram | other -- libre'),
1530
+ type: z.string().optional().describe('facebook | instagram | tiktok | linkedin | x | blog | newsletter | other -- libre. facebook/instagram/tiktok/linkedin se conectan después con connect_postproxy_channel'),
1522
1531
  handle: z.string().optional().describe('Ej. "@atlaspagos" o una URL/dominio'),
1523
1532
  color: z.string().optional(),
1524
1533
  cadenceCount: z.number().optional().describe('Ritmo objetivo de publicaciones, ej. 2'),
@@ -1632,24 +1641,26 @@ export function createServer() {
1632
1641
  // Un agente tiene un `kind` atado a una máquina (runnerId); los 3 kinds de
1633
1642
  // hoy (23 ago 2026: qa/security/performance) exigen stack+localPath (un
1634
1643
  // checkout real en esa máquina), y performance además perfUrl (URL viva
1635
- // contra la que corre Lighthouse). Los 3 solo tienen lógica real para
1636
- // stack="web" por ahora. "orquestra-qa-agent setup" crea/actualiza solo
1637
- // agentes kind="qa" -- security/performance se crean a mano acá o desde
1638
- // la web.
1639
- const AGENT_KINDS = ['qa', 'security', 'performance']
1644
+ // contra la que corre Lighthouse). qa/security/performance solo tienen
1645
+ // lógica real para stack="web" por ahora; screenshots es lo inverso, solo
1646
+ // stack="ios"|"android" (captura automática para el checklist de
1647
+ // publicación, ver ScreenshotChecklist.jsx en orquestra-web). "orquestra-qa-agent
1648
+ // setup" crea/actualiza solo agentes kind="qa" -- el resto se crean a
1649
+ // mano acá o desde la web.
1650
+ const AGENT_KINDS = ['qa', 'security', 'performance', 'screenshots']
1640
1651
  const AGENT_STACKS = ['web', 'ios', 'android', 'node', 'nestjs']
1641
1652
 
1642
1653
  registerRead('list_agents', '/v1/agents',
1643
1654
  'Lista los agentes (kind+máquina+stack+ruta local) configurados en un proyecto. Úsala antes de create_agent o de create_agent_run con stack, para saber si ya existe uno.',
1644
- { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks) o "performance" (build size + Lighthouse)') },
1655
+ { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
1645
1656
  (args, projectId) => ({ projectId }))
1646
1657
 
1647
- registerCreate('create_agent', '/v1/agents', 'Crea un agente: asocia un kind ("qa" | "security" | "performance") a una máquina (runner, ver Workspace → Máquinas), un stack de este proyecto y una ruta de checkout en ESA máquina. kind="performance" además necesita perfUrl.', {
1658
+ registerCreate('create_agent', '/v1/agents', 'Crea un agente: asocia un kind ("qa" | "security" | "performance" | "screenshots") a una máquina (runner, ver Workspace → Máquinas), un stack de este proyecto y una ruta de checkout en ESA máquina. kind="performance" además necesita perfUrl.', {
1648
1659
  name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
1649
- kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks) o "performance" (build size + Lighthouse) -- default "qa"'),
1660
+ kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
1650
1661
  runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
1651
- stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 3 kinds -- security/performance solo tienen lógica real para "web" hoy'),
1652
- localPath: z.string().optional().describe('Requerido para los 3 kinds -- ruta absoluta del checkout en esa máquina'),
1662
+ stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 4 kinds. security/performance solo tienen lógica real para "web" hoy; screenshots solo para "ios"/"android"'),
1663
+ localPath: z.string().optional().describe('Requerido para los 4 kinds -- ruta absoluta del checkout en esa máquina'),
1653
1664
  branch: z.string().optional().describe('Rama de este agente -- default "main" si no se manda'),
1654
1665
  distributionId: z.string().optional().describe('Distribución vinculada (repo/fuente de conocimiento)'),
1655
1666
  enabled: z.boolean().optional(),
@@ -1684,7 +1695,7 @@ export function createServer() {
1684
1695
  minPerfScore: z.number().optional(),
1685
1696
  })
1686
1697
 
1687
- registerCreate('create_agent_run', '/v1/agent-runs', 'Dispara una corrida de agente (qa: lint/tests/build/E2E; security: npm audit + gitleaks; performance: build size + Lighthouse) en la rama indicada. Necesita al menos un agente activo para el stack (o falla explícito -- ver list_agents/create_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_agent_run.', {
1698
+ registerCreate('create_agent_run', '/v1/agent-runs', 'Dispara una corrida de agente (qa: lint/tests/build/E2E; security: npm audit + gitleaks; performance: build size + Lighthouse; screenshots: captura de pantallas para el checklist de publicación) en la rama indicada. Necesita al menos un agente activo para el stack (o falla explícito -- ver list_agents/create_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_agent_run.', {
1688
1699
  agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack/kind'),
1689
1700
  branch: z.string().optional().describe('Rama a probar -- default: la del agente, o "main"'),
1690
1701
  commit: z.string().optional(),
@@ -1692,7 +1703,7 @@ export function createServer() {
1692
1703
  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_agent) o más de 1 (usa agentId o suma kind para desambiguar).'),
1693
1704
  kind: z.enum(AGENT_KINDS).optional().describe('Desambigua junto con stack cuando el proyecto tiene más de un agente para el mismo stack (ej. un "qa" y un "security", ambos "web") -- sin esto, resuelve por stack solo y falla explícito si hay más de uno.'),
1694
1705
  runnerId: z.string().optional().describe('Máquina específica -- si se manda, gana sobre la del agente resuelto'),
1695
- taskId: z.string().optional().describe('Acota la revisión con IA (si el agente la tiene activada) a esta tarea en vez de "revisa todo el proyecto" -- útil para pedir "corre QA de esto que acabo de hacer". Sin efecto en los pasos de script (lint/test/build corren igual siempre). No combinar con moduleId.'),
1706
+ taskId: z.string().optional().describe('Acota la revisión con IA (si el agente la tiene activada) a esta tarea en vez de "revisa todo el proyecto" -- útil para pedir "corre QA de esto que acabo de hacer". Sin efecto en los pasos de script (lint/test/build corren igual siempre). Para kind="screenshots" es además OBLIGATORIO en la práctica: es la tarea a la que se van a adjuntar solas las capturas al terminar la corrida -- sin taskId, las capturas se suben pero nadie las liga a nada. No combinar con moduleId.'),
1696
1707
  moduleId: z.string().optional().describe('Igual que taskId pero acota a un módulo completo. No combinar con taskId.'),
1697
1708
  })
1698
1709