orquestra-mcp 1.16.0 → 1.16.3
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 +11 -9
- package/package.json +1 -1
- package/src/platformPlaybooks.mjs +22 -2
- package/src/server.mjs +67 -39
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# orquestra-mcp
|
|
2
2
|
|
|
3
|
-
Servidor MCP (Model Context Protocol) para Orquestra. Expone **
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|
|
3
|
+
"version": "1.16.3",
|
|
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": {
|
|
@@ -71,6 +71,11 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
71
71
|
title: 'Eliminar cuenta desde la app (Guideline 5.1.1(v))',
|
|
72
72
|
body: 'Obligatorio desde 2022: si la app permite crear cuenta, tiene que permitir ELIMINARLA desde DENTRO de la app -- "escríbenos por correo" no cumple el requisito, aunque sea rápido y real. Verifica que exista un flujo de borrado self-service (botón "Eliminar cuenta" en Perfil/Ajustes, con confirmación) ANTES de enviar a revisión -- si no existe, Apple puede rechazar la app durante el review, incluso si el resto de la metadata está perfecta. Orquestra ya tiene este flujo (Zona de riesgo en Perfil, backend server-side que borra cuenta+datos), úsalo de referencia de implementación si tu app lo necesita desde cero.',
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
key: 'sign_in_apple',
|
|
76
|
+
title: 'Sign in with Apple si hay login de terceros (Guideline 4.8)',
|
|
77
|
+
body: 'Si la app ofrece login con Google, Facebook u otro proveedor de terceros, Apple exige que TAMBIÉN ofrezcas "Sign in with Apple" como alternativa equivalente -- Guideline 4.8 ("Login Services"). No es una recomendación: si falta, Apple RECHAZA el build completo en revisión (motivo real, no hipotético) sin importar que el resto de la metadata esté perfecta. Implementación de referencia: botón nativo (`AuthenticationServices`, nonce) + capability "Sign in with Apple" habilitada en el App ID (Apple Developer Portal) + provider "Apple" activado en Firebase Auth si se usa Firebase. Si el login es solo email/password propio, sin ningún proveedor de terceros, esta regla no aplica.',
|
|
78
|
+
},
|
|
74
79
|
],
|
|
75
80
|
|
|
76
81
|
tasks: [
|
|
@@ -97,7 +102,7 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
97
102
|
{
|
|
98
103
|
title: 'Preparar capturas de pantalla en los tamaños exactos que pide App Store Connect',
|
|
99
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.',
|
|
100
|
-
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 7,
|
|
105
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 7, checklistTool: 'screenshots',
|
|
101
106
|
},
|
|
102
107
|
{
|
|
103
108
|
title: 'Completar el cuestionario de privacidad de la tienda (Data Collection)',
|
|
@@ -114,6 +119,11 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
114
119
|
description: 'Guideline 5.1.1(v), obligatorio -- "escríbenos por correo" NO cumple el requisito. Tiene que haber un botón real de "Eliminar cuenta" (con confirmación) en Perfil/Ajustes que borre la cuenta y los datos sin salir de la app. Si no existe, constrúyelo ANTES de mandar a revisión, no después de un rechazo.',
|
|
115
120
|
type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
|
|
116
121
|
},
|
|
122
|
+
{
|
|
123
|
+
title: 'Verificar que si hay login de terceros (Google/Facebook/etc.), también esté "Sign in with Apple"',
|
|
124
|
+
description: 'Guideline 4.8, obligatorio -- si la app ofrece login con Google, Facebook u otro proveedor externo, Apple rechaza la app en revisión si no ofrece también "Sign in with Apple" como alternativa. No aplica si el login es solo email/password propio, sin proveedores de terceros. Si falta, agrégalo ANTES de mandar a revisión, no después de un rechazo.',
|
|
125
|
+
type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
|
|
126
|
+
},
|
|
117
127
|
],
|
|
118
128
|
},
|
|
119
129
|
|
|
@@ -190,6 +200,11 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
190
200
|
title: 'App access / Sign-in details',
|
|
191
201
|
body: 'Si la app requiere login, "Add sign in details" pide un set de credenciales de prueba REALES (nombre descriptivo del set, usuario/email, contraseña) que el reviewer va a usar de verdad para entrar -- tienen que funcionar y, si es posible, mostrar contenido de ejemplo real (no una cuenta vacía cayendo en un onboarding). Marca el checkbox de "esta cuenta da acceso completo a todas las funciones" si no hay niveles pagos/premium.',
|
|
192
202
|
},
|
|
203
|
+
{
|
|
204
|
+
key: 'google_signin_sha1',
|
|
205
|
+
title: 'Google Sign-In: SHA-1 de las TRES firmas en Firebase (debug, upload key y Play App Signing)',
|
|
206
|
+
body: 'Si la app tiene "Continuar con Google", Google valida la firma del APK contra los SHA-1 registrados en Firebase → Configuración del proyecto → app Android → "Agregar huella digital". Son TRES firmas distintas y cada una necesita su SHA-1: (1) debug (`~/.android/debug.keystore`, builds locales), (2) upload key (el keystore con el que firmas el AAB, `assembleRelease`), y (3) Play App Signing -- la clave con la que Google RE-FIRMA los APK que instalan testers y usuarios desde Play; NO es la upload key, y es la que siempre se olvida. Se ve en Play Console → Test and release → App integrity → Play Store protection → Play app signing → "App signing key certificate" (columna Classical key). Las dos primeras salen de `./gradlew signingReport`. Síntoma si falta alguna: al elegir la cuenta "no pasa nada" -- Play Services lo reporta como cancelación del usuario, no como error de configuración, así que no hay ningún mensaje que te avise. Después de registrarlas, vuelve a descargar google-services.json.',
|
|
207
|
+
},
|
|
193
208
|
{
|
|
194
209
|
key: 'delete_account',
|
|
195
210
|
title: 'Eliminar cuenta desde la app (política de borrado de cuenta de Google Play)',
|
|
@@ -228,10 +243,15 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
228
243
|
description: 'Política de borrado de cuenta de Google Play (equivalente a la Guideline 5.1.1(v) de Apple) -- lo ideal es un flujo real dentro de la app, no solo la "Delete account URL" de la ficha. Si no existe, constrúyelo antes de publicar.',
|
|
229
244
|
type: 'security', priority: 'high', tags: ['publicacion', 'android', 'compliance'], offsetDays: 1,
|
|
230
245
|
},
|
|
246
|
+
{
|
|
247
|
+
title: 'Registrar en Firebase el SHA-1 de las TRES firmas (debug, upload key y Play App Signing) si la app usa Google Sign-In',
|
|
248
|
+
description: 'Gotcha real: cada firma distinta del APK necesita su propio SHA-1 en Firebase (Configuración del proyecto → app Android → "Agregar huella digital"), y la que siempre se olvida es la de Play App Signing -- Google re-firma con ella los APK que se instalan desde Play, así que un login con Google que funciona en debug y en el release local falla en el build de los testers sin ningún mensaje (Play Services lo reporta como cancelación). Debug y upload key: `./gradlew signingReport`. Play App Signing: Play Console → App integrity → Play app signing → "App signing key certificate". Al terminar, re-descargar google-services.json.',
|
|
249
|
+
type: 'tech', priority: 'high', tags: ['publicacion', 'android', 'google-sign-in'], offsetDays: 1,
|
|
250
|
+
},
|
|
231
251
|
{
|
|
232
252
|
title: 'Preparar capturas de pantalla en los tamaños exactos que pide Play Console',
|
|
233
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.',
|
|
234
|
-
type: 'business', priority: 'medium', tags: ['publicacion', 'android'], offsetDays: 7,
|
|
254
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'android'], offsetDays: 7, checklistTool: 'screenshots',
|
|
235
255
|
},
|
|
236
256
|
{
|
|
237
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 }
|
|
@@ -115,7 +119,7 @@ Si el usuario pregunta explícito qué Claude Skills de terceros le faltan (o pi
|
|
|
115
119
|
|
|
116
120
|
export function createServer() {
|
|
117
121
|
const config = loadConfig()
|
|
118
|
-
const server = new McpServer({ name: 'orquestra-mcp', version:
|
|
122
|
+
const server = new McpServer({ name: 'orquestra-mcp', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS })
|
|
119
123
|
|
|
120
124
|
function resolveProjectId(projectId) {
|
|
121
125
|
if (config.defaultProjectId) return config.defaultProjectId
|
|
@@ -613,10 +617,10 @@ export function createServer() {
|
|
|
613
617
|
server.registerTool(
|
|
614
618
|
'connect_postproxy_channel',
|
|
615
619
|
{
|
|
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} --
|
|
620
|
+
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
621
|
inputSchema: {
|
|
618
622
|
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'),
|
|
623
|
+
channelId: z.string().describe('Canal a conectar (ver list_channels) -- su `type` tiene que ser facebook/instagram/tiktok/linkedin'),
|
|
620
624
|
platform: z.string().describe('Plataforma tal cual la espera Postproxy (mismo valor que channel.type -- ej. "instagram")'),
|
|
621
625
|
},
|
|
622
626
|
},
|
|
@@ -1518,7 +1522,7 @@ export function createServer() {
|
|
|
1518
1522
|
|
|
1519
1523
|
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
1524
|
name: z.string().describe('Ej. "LinkedIn"'),
|
|
1521
|
-
type: z.string().optional().describe('linkedin | x | blog | newsletter | instagram
|
|
1525
|
+
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
1526
|
handle: z.string().optional().describe('Ej. "@atlaspagos" o una URL/dominio'),
|
|
1523
1527
|
color: z.string().optional(),
|
|
1524
1528
|
cadenceCount: z.number().optional().describe('Ritmo objetivo de publicaciones, ej. 2'),
|
|
@@ -1589,6 +1593,8 @@ export function createServer() {
|
|
|
1589
1593
|
saves: z.number().optional().describe('Guardados -- entrada manual, típico de X/Instagram (solo con channelId suelto)'),
|
|
1590
1594
|
avgReadMinutes: z.number().optional().describe('Minutos de lectura media -- entrada manual, típico de blog (solo con channelId suelto)'),
|
|
1591
1595
|
imagePrompt: z.string().optional().describe('Descripción visual en inglés para generar la portada -- no genera la imagen, solo guarda el texto (solo con channelId suelto)'),
|
|
1596
|
+
recommendedMedia: z.enum(['image', 'video']).optional().describe('Qué conviene más de portada para esta pieza -- puramente informativo, ningún flujo cambia solo por esto (solo con channelId suelto)'),
|
|
1597
|
+
videoPrompt: z.string().optional().describe('Análogo a imagePrompt pero para video: descripción de la escena en movimiento, en inglés, para pegar en un generador de video externo -- Orquestra no genera video, solo guarda el texto (solo con channelId suelto)'),
|
|
1592
1598
|
language: z.string().optional().describe('\'en\' si esta pieza es la traducción de otra. Sin valor = idioma original.'),
|
|
1593
1599
|
translationOf: z.string().optional().describe('Si esta pieza es una traducción, id del content original'),
|
|
1594
1600
|
})
|
|
@@ -1616,6 +1622,8 @@ export function createServer() {
|
|
|
1616
1622
|
saves: z.number().optional(),
|
|
1617
1623
|
avgReadMinutes: z.number().optional(),
|
|
1618
1624
|
imagePrompt: z.string().optional(),
|
|
1625
|
+
recommendedMedia: z.enum(['image', 'video']).optional(),
|
|
1626
|
+
videoPrompt: z.string().optional(),
|
|
1619
1627
|
language: z.string().optional(),
|
|
1620
1628
|
translationOf: z.string().optional(),
|
|
1621
1629
|
})
|
|
@@ -1628,24 +1636,26 @@ export function createServer() {
|
|
|
1628
1636
|
// Un agente tiene un `kind` atado a una máquina (runnerId); los 3 kinds de
|
|
1629
1637
|
// hoy (23 ago 2026: qa/security/performance) exigen stack+localPath (un
|
|
1630
1638
|
// checkout real en esa máquina), y performance además perfUrl (URL viva
|
|
1631
|
-
// contra la que corre Lighthouse).
|
|
1632
|
-
// stack="web" por ahora
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1639
|
+
// contra la que corre Lighthouse). qa/security/performance solo tienen
|
|
1640
|
+
// lógica real para stack="web" por ahora; screenshots es lo inverso, solo
|
|
1641
|
+
// stack="ios"|"android" (captura automática para el checklist de
|
|
1642
|
+
// publicación, ver ScreenshotChecklist.jsx en orquestra-web). "orquestra-qa-agent
|
|
1643
|
+
// setup" crea/actualiza solo agentes kind="qa" -- el resto se crean a
|
|
1644
|
+
// mano acá o desde la web.
|
|
1645
|
+
const AGENT_KINDS = ['qa', 'security', 'performance', 'screenshots']
|
|
1636
1646
|
const AGENT_STACKS = ['web', 'ios', 'android', 'node', 'nestjs']
|
|
1637
1647
|
|
|
1638
1648
|
registerRead('list_agents', '/v1/agents',
|
|
1639
1649
|
'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.',
|
|
1640
|
-
{ kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks)
|
|
1650
|
+
{ 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)') },
|
|
1641
1651
|
(args, projectId) => ({ projectId }))
|
|
1642
1652
|
|
|
1643
|
-
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.', {
|
|
1653
|
+
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.', {
|
|
1644
1654
|
name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
|
|
1645
|
-
kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks)
|
|
1655
|
+
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"'),
|
|
1646
1656
|
runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
|
|
1647
|
-
stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los
|
|
1648
|
-
localPath: z.string().optional().describe('Requerido para los
|
|
1657
|
+
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"'),
|
|
1658
|
+
localPath: z.string().optional().describe('Requerido para los 4 kinds -- ruta absoluta del checkout en esa máquina'),
|
|
1649
1659
|
branch: z.string().optional().describe('Rama de este agente -- default "main" si no se manda'),
|
|
1650
1660
|
distributionId: z.string().optional().describe('Distribución vinculada (repo/fuente de conocimiento)'),
|
|
1651
1661
|
enabled: z.boolean().optional(),
|
|
@@ -1680,7 +1690,7 @@ export function createServer() {
|
|
|
1680
1690
|
minPerfScore: z.number().optional(),
|
|
1681
1691
|
})
|
|
1682
1692
|
|
|
1683
|
-
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.', {
|
|
1693
|
+
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.', {
|
|
1684
1694
|
agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack/kind'),
|
|
1685
1695
|
branch: z.string().optional().describe('Rama a probar -- default: la del agente, o "main"'),
|
|
1686
1696
|
commit: z.string().optional(),
|
|
@@ -1688,7 +1698,7 @@ export function createServer() {
|
|
|
1688
1698
|
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).'),
|
|
1689
1699
|
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.'),
|
|
1690
1700
|
runnerId: z.string().optional().describe('Máquina específica -- si se manda, gana sobre la del agente resuelto'),
|
|
1691
|
-
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.'),
|
|
1701
|
+
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.'),
|
|
1692
1702
|
moduleId: z.string().optional().describe('Igual que taskId pero acota a un módulo completo. No combinar con taskId.'),
|
|
1693
1703
|
})
|
|
1694
1704
|
|
|
@@ -1812,7 +1822,7 @@ export function createServer() {
|
|
|
1812
1822
|
server.registerTool(
|
|
1813
1823
|
'apply_platform_playbook',
|
|
1814
1824
|
{
|
|
1815
|
-
description: 'Aplica el Platform Playbook de publicación (iOS App Store / Google Play) sobre una distribución real: crea el módulo + tareas de la plantilla (igual que apply_stage_playbook) y, si mandás publishForm, también completa el formulario de la Guía de publicación de esa distribución (mismo dato que llena DistributionGuide.jsx en la web). Solo completá campos para los que tengas una base real del proyecto (código, get_project_context, servicios/entornos configurados) -- nunca inventes pricing, availability, package/bundle ID, cuentas demo para el reviewer, o el nombre exacto de la app; dejalos afuera de publishForm para que el usuario los complete a mano. Si la distribución YA tiene valores guardados en alguna de las claves que mandás, la tool NO los sobreescribe en silencio -- devuelve publishFormConflict con el valor actual vs. el propuesto por cada clave en conflicto, sin tocar publishForm. Mostrale ese conflicto al usuario y volvé a llamar con confirmReplace:true SOLO si confirma explícitamente el reemplazo.',
|
|
1825
|
+
description: 'Aplica el Platform Playbook de publicación (iOS App Store / Google Play) sobre una distribución real: crea el módulo + tareas de la plantilla (igual que apply_stage_playbook) y, si mandás publishForm, también completa el formulario de la Guía de publicación de esa distribución (mismo dato que llena DistributionGuide.jsx en la web). Solo completá campos para los que tengas una base real del proyecto (código, get_project_context, servicios/entornos configurados) -- nunca inventes pricing, availability, package/bundle ID, cuentas demo para el reviewer, o el nombre exacto de la app; dejalos afuera de publishForm para que el usuario los complete a mano. Si la distribución YA tiene valores guardados en alguna de las claves que mandás, la tool NO los sobreescribe en silencio -- devuelve publishFormConflict con el valor actual vs. el propuesto por cada clave en conflicto, sin tocar publishForm. Mostrale ese conflicto al usuario y volvé a llamar con confirmReplace:true SOLO si confirma explícitamente el reemplazo. Si este playbook YA se aplicó antes sobre esta misma distribución (módulo+tareas ya creados), la tool lo detecta sola (mismo mecanismo que el badge "Ya aplicado" de DistributionGuide.jsx) y NO vuelve a crear módulo/tareas duplicados -- devuelve alreadyApplied:true. Volvé a llamar con force:true SOLO si el usuario pide explícitamente un checklist nuevo encima del que ya existe.',
|
|
1816
1826
|
inputSchema: {
|
|
1817
1827
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1818
1828
|
platform: z.enum(['ios', 'android']).describe('ios (App Store Connect) | android (Google Play Console) -- ver get_platform_playbook para el contenido'),
|
|
@@ -1820,40 +1830,58 @@ export function createServer() {
|
|
|
1820
1830
|
moduleName: z.string().optional().describe('Nombre del módulo -- default: el de la plantilla'),
|
|
1821
1831
|
publishForm: z.record(z.string()).optional().describe('Respuestas a completar, clave = key de referenceGuide (ver get_platform_playbook). Omitido o vacío = solo crea módulo + tareas, sin tocar el formulario.'),
|
|
1822
1832
|
confirmReplace: z.boolean().optional().describe('Default false. Mandá true solo cuando el usuario ya confirmó explícitamente reemplazar valores existentes que te devolvió publishFormConflict en un llamado anterior.'),
|
|
1833
|
+
force: z.boolean().optional().describe('Default false. Si este playbook ya se aplicó antes sobre esta distribución, por default la tool NO crea un segundo módulo/checklist -- mandá true solo cuando el usuario pidió explícitamente crear uno nuevo encima del que ya existe.'),
|
|
1823
1834
|
},
|
|
1824
1835
|
},
|
|
1825
|
-
async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace }) => {
|
|
1836
|
+
async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace, force }) => {
|
|
1826
1837
|
try {
|
|
1827
1838
|
const pid = resolveProjectId(projectId)
|
|
1828
1839
|
const playbook = PLATFORM_PLAYBOOKS[platform]
|
|
1829
|
-
const
|
|
1830
|
-
projectId: pid,
|
|
1831
|
-
name: moduleName || playbook.moduleName,
|
|
1832
|
-
description: playbook.moduleDescription,
|
|
1833
|
-
tags: playbook.moduleTags,
|
|
1834
|
-
})
|
|
1840
|
+
const appliedKey = `${distributionId}_publish`
|
|
1835
1841
|
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1842
|
+
const project = await apiGet(config, `/v1/projects/${pid}`, {})
|
|
1843
|
+
const alreadyApplied = !!(project.appliedPlaybooks || {})[appliedKey]
|
|
1844
|
+
|
|
1845
|
+
const result = {}
|
|
1846
|
+
|
|
1847
|
+
if (alreadyApplied && !force) {
|
|
1848
|
+
result.alreadyApplied = true
|
|
1849
|
+
result.message = 'Este playbook ya se había aplicado antes sobre esta distribución (el módulo y sus tareas ya existen en el proyecto) -- no se creó nada de nuevo para no duplicarlo. Si el usuario pide explícitamente un checklist nuevo, volvé a llamar con force:true.'
|
|
1850
|
+
} else {
|
|
1851
|
+
const moduleData = await apiPost(config, '/v1/modules', {
|
|
1840
1852
|
projectId: pid,
|
|
1841
|
-
|
|
1842
|
-
description:
|
|
1843
|
-
|
|
1844
|
-
priority: t.priority,
|
|
1845
|
-
tags: t.tags,
|
|
1846
|
-
moduleId: moduleData.id,
|
|
1847
|
-
dueDate,
|
|
1853
|
+
name: moduleName || playbook.moduleName,
|
|
1854
|
+
description: playbook.moduleDescription,
|
|
1855
|
+
tags: playbook.moduleTags,
|
|
1848
1856
|
})
|
|
1849
|
-
taskIds.push(created.id)
|
|
1850
|
-
}
|
|
1851
1857
|
|
|
1852
|
-
|
|
1858
|
+
const taskIds = []
|
|
1859
|
+
for (const t of playbook.tasks) {
|
|
1860
|
+
const dueDate = new Date(Date.now() + t.offsetDays * 86400000).toISOString().slice(0, 10)
|
|
1861
|
+
const created = await apiPost(config, '/v1/tasks', {
|
|
1862
|
+
projectId: pid,
|
|
1863
|
+
title: t.title,
|
|
1864
|
+
description: t.description,
|
|
1865
|
+
type: t.type,
|
|
1866
|
+
priority: t.priority,
|
|
1867
|
+
tags: t.tags,
|
|
1868
|
+
moduleId: moduleData.id,
|
|
1869
|
+
dueDate,
|
|
1870
|
+
})
|
|
1871
|
+
taskIds.push(created.id)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
result.module = moduleData
|
|
1875
|
+
result.tasksCreated = taskIds.length
|
|
1876
|
+
result.taskIds = taskIds
|
|
1877
|
+
|
|
1878
|
+
await apiPatch(config, '/v1/projects', { id: pid, appliedPlaybooks: { [appliedKey]: true } })
|
|
1879
|
+
result.markedApplied = true
|
|
1880
|
+
}
|
|
1853
1881
|
|
|
1854
1882
|
if (publishForm && Object.keys(publishForm).length > 0) {
|
|
1855
1883
|
const distributions = await apiGet(config, '/v1/distributions', { projectId: pid })
|
|
1856
|
-
const dist = distributions.find(d => d.id === distributionId)
|
|
1884
|
+
const dist = distributions.items.find(d => d.id === distributionId)
|
|
1857
1885
|
if (!dist) {
|
|
1858
1886
|
throw new Error(`No se encontró la distribución "${distributionId}" en este proyecto -- revisá list_distributions.`)
|
|
1859
1887
|
}
|