orquestra-mcp 1.9.0 → 1.14.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 +1 -1
- package/package.json +3 -3
- package/src/api.mjs +13 -0
- package/src/foundations.mjs +50 -0
- package/src/server.mjs +301 -34
- package/src/stages.mjs +3 -2
package/README.md
CHANGED
|
@@ -189,7 +189,7 @@ create/update sigue siendo criterio del agente y del humano.
|
|
|
189
189
|
38 tools: 9 de lectura (contexto/tasks/search + dashboard/workspace/infra/
|
|
190
190
|
paridad/setup guide/stage playbook) + 6 `list_*` + 19 de creación/edición
|
|
191
191
|
(tasks/ideas/changelog/notes/docs + módulos/flujo/categorías/servicios/
|
|
192
|
-
entornos/distribuciones/proyecto) + 2 de
|
|
192
|
+
entornos/distribuciones/proyecto) + 2 de Agentes (create_agent_run/get_agent_run) +
|
|
193
193
|
2 compuestas (init_project, apply_stage_playbook). No hay `delete_*`, ni
|
|
194
194
|
`update_idea`/`update_note`/`update_doc`/`update_changelog_entry`
|
|
195
195
|
todavía. Publicado en npm como `orquestra-mcp` (unscoped) -- `npx
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Servidor MCP de Orquestra --
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "Servidor MCP de Orquestra -- 78 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
|
-
"orquestra-mcp": "
|
|
7
|
+
"orquestra-mcp": "bin/orquestra-mcp.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/api.mjs
CHANGED
|
@@ -102,3 +102,16 @@ export async function apiDelete(config, path, { projectId, id }) {
|
|
|
102
102
|
})
|
|
103
103
|
return parseResponse(res)
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
// PUT crudo a una presigned URL de S3 (createAttachmentUploadUrl en
|
|
107
|
+
// orquestra-infra) -- a diferencia de apiGet/apiPost/etc, este request NO
|
|
108
|
+
// va contra config.apiBase ni lleva el Bearer del MCP: la URL firmada YA
|
|
109
|
+
// es la autorización completa.
|
|
110
|
+
export async function putToPresignedUrl(uploadUrl, buffer, contentType) {
|
|
111
|
+
const res = await fetch(uploadUrl, {
|
|
112
|
+
method: 'PUT',
|
|
113
|
+
headers: { 'Content-Type': contentType },
|
|
114
|
+
body: buffer,
|
|
115
|
+
})
|
|
116
|
+
if (!res.ok) throw new Error(`Falló la subida a S3: HTTP ${res.status}`)
|
|
117
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Checklist de "fundamentos olvidados" -- prácticas chicas que casi ningún
|
|
2
|
+
// proyecto instrumenta por su cuenta pero suman mucho cuando faltan (idea
|
|
3
|
+
// de José, 21 ago 2026, a raíz de auditar Orquestra mismo y encontrar
|
|
4
|
+
// analytics/crash reporting ausentes en las 4 apps). Constantes estáticas,
|
|
5
|
+
// sin acceso a red ni a la base de datos -- mismo criterio que stages.mjs.
|
|
6
|
+
// get_foundations_checklist las expone tal cual; el agente conectado hace
|
|
7
|
+
// la detección real (grep del repo local) y usa report_foundation_gap para
|
|
8
|
+
// cada una que confirme ausente -- ver SERVER_INSTRUCTIONS.
|
|
9
|
+
export const FOUNDATIONS = [
|
|
10
|
+
{
|
|
11
|
+
key: 'analytics',
|
|
12
|
+
label: 'Analytics de producto',
|
|
13
|
+
category: 'observabilidad',
|
|
14
|
+
why: 'Sin eventos básicos (pantallas, acciones clave) no hay forma de saber qué usa la gente ni de tomar decisiones con datos.',
|
|
15
|
+
detectionHint: 'Buscar SDKs de analytics en dependencias (Firebase Analytics, Segment, Amplitude, PostHog, Mixpanel) o gtag/GA4 en el HTML -- y confirmar que de verdad se llame (logEvent/track), no solo que esté listada la dependencia.',
|
|
16
|
+
scales: ['app', 'platform'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: 'crash_reporting',
|
|
20
|
+
label: 'Reporte de errores y crashes',
|
|
21
|
+
category: 'observabilidad',
|
|
22
|
+
why: 'Sin esto, los crashes de usuarios reales son invisibles -- solo se enteran si alguien reporta a mano.',
|
|
23
|
+
detectionHint: 'Buscar Crashlytics/Sentry/Bugsnag en dependencias, y confirmar inicialización real en el entrypoint de la app (no solo la dependencia listada y sin usar).',
|
|
24
|
+
scales: ['app', 'platform'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'error_alerting',
|
|
28
|
+
label: 'Alertas de error del backend',
|
|
29
|
+
category: 'observabilidad',
|
|
30
|
+
why: 'Un log que nadie lee no es monitoreo -- sin una alarma real, un backend caído puede tardar horas o días en notarse.',
|
|
31
|
+
detectionHint: 'Buscar alarmas de CloudWatch con AlarmActions/SNS, integraciones de Sentry/PagerDuty, o un webhook de errores a Slack/email en la infraestructura.',
|
|
32
|
+
scales: ['platform'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'dependency_scanning',
|
|
36
|
+
label: 'Escaneo de vulnerabilidades de dependencias',
|
|
37
|
+
category: 'seguridad',
|
|
38
|
+
why: 'Dependencias con vulnerabilidades conocidas quedan sin parchear indefinidamente si nada avisa cuando aparecen.',
|
|
39
|
+
detectionHint: 'Buscar .github/dependabot.yml, renovate.json, o un paso de audit (npm audit / pip-audit / similar) en el CI.',
|
|
40
|
+
scales: ['app', 'platform'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'ci_pipeline',
|
|
44
|
+
label: 'CI antes de mergear',
|
|
45
|
+
category: 'calidad',
|
|
46
|
+
why: 'Sin lint/tests/build automáticos en cada PR, cada regresión depende de que alguien la note a mano antes de mergear.',
|
|
47
|
+
detectionHint: 'Buscar workflows de CI (.github/workflows/*.yml, GitLab CI, CircleCI) con jobs que corran en pull_request, no solo en push a main.',
|
|
48
|
+
scales: ['app', 'platform'],
|
|
49
|
+
},
|
|
50
|
+
]
|
package/src/server.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
1
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
4
|
import { z } from 'zod'
|
|
3
|
-
import { loadConfig, apiGet, apiPost, apiPatch, apiDelete, ApiError } from './api.mjs'
|
|
5
|
+
import { loadConfig, apiGet, apiPost, apiPatch, apiDelete, putToPresignedUrl, ApiError } from './api.mjs'
|
|
4
6
|
import { STAGES, PLAYBOOKS } from './stages.mjs'
|
|
7
|
+
import { FOUNDATIONS } from './foundations.mjs'
|
|
5
8
|
|
|
6
9
|
function errorResult(err) {
|
|
7
10
|
const text = err instanceof ApiError ? `Error (${err.status}): ${err.message}` : `Error: ${err.message}`
|
|
@@ -35,6 +38,15 @@ const PLATFORM_TYPES = ['android', 'ios', 'web', 'desktop-mac', 'desktop-win', '
|
|
|
35
38
|
const INFRA_STATUSES = ['operational', 'degraded', 'down']
|
|
36
39
|
const STAGE_IDS = STAGES.map(s => s.id)
|
|
37
40
|
|
|
41
|
+
// Adjuntos -- entityType en la tool es singular (task/note/idea/doc, más
|
|
42
|
+
// natural para el modelo), la API usa el plural de la colección de
|
|
43
|
+
// Firestore (tasks/notes/ideas/docs) -- este mapa traduce entre los dos.
|
|
44
|
+
const ATTACHMENT_ENTITY_PATH = { task: 'tasks', note: 'notes', idea: 'ideas', doc: 'docs' }
|
|
45
|
+
const ATTACHMENT_ENTITY_TYPES = Object.keys(ATTACHMENT_ENTITY_PATH)
|
|
46
|
+
const ATTACHMENT_IMAGE_MIME_BY_EXT = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' }
|
|
47
|
+
const MAX_ATTACHMENT_IMAGE_BYTES = 15 * 1024 * 1024
|
|
48
|
+
const MAX_ATTACHMENT_CODE_CHARS = 200_000
|
|
49
|
+
|
|
38
50
|
const SETUP_GUIDE = {
|
|
39
51
|
intro: 'Para configurar o sincronizar un proyecto completo: revisa primero con la tool de "check" de cada paso; si ya existe algo que coincide por name/type/title, edítalo con la tool de "update"; si no existe, créalo con la de "create". Sigue el orden -- los pasos 4 en adelante referencian ids de los pasos 1-3 (statusId, categoryId, moduleId, distId).',
|
|
40
52
|
steps: [
|
|
@@ -65,6 +77,8 @@ Si get_project_context devuelve un proyecto vacío (sin módulos ni tareas), es
|
|
|
65
77
|
- Si es una idea nueva: llama a get_setup_guide y ofrece un playbook de etapa "pre" (get_stage_playbook + apply_stage_playbook) -- ahí sí basta con eso, el proyecto de verdad no tiene nada más que sincronizar todavía.
|
|
66
78
|
- Si ya existe y está avanzado: NO te limites a update_project ni a las distributions -- eso es solo el primer paso, no el trabajo completo. Llama a get_setup_guide y recorre sus 9 pasos en orden real (estatus/flujo, categorías, infraestructura completa -- servicios Y entornos, no solo distribuciones --, módulos, tareas, ideas, notas, changelog, docs), usando list_* de cada uno antes de crear para no duplicar. No asumas ni dejes vacío lo que no puedas inferir del código -- pregúntale al usuario los servicios/entornos reales, los módulos o áreas de trabajo, y las tareas pendientes reales que tenga en mente. Ofrece el playbook de etapa "build" (no "pre") solo como plantilla de módulo+tareas inicial, no como sustituto de sincronizar el resto.
|
|
67
79
|
|
|
80
|
+
Al aplicar un playbook con apply_stage_playbook -- sobre todo el de etapa "post" (marketing/contenido) -- nunca asumas herramientas o canales que este proyecto no tiene registrados en Orquestra. Antes de crear tareas de contenido/publicación, llama a list_channels: si viene vacío, la plantilla ya incluye una tarea para conectar canales reales primero -- no la saltees ni generes tareas del tipo "publica en Instagram/LinkedIn/TikTok" para un canal que el proyecto no tiene. Si list_channels SÍ trae canales reales, no te quedes solo con la tarea recordatorio de "programa tu contenido" -- además creá piezas REALES con create_content (un plan de lanzamiento concreto: teaser, anuncio, demo del caso de uso, historia de por qué se construyó, 1-2 testimonios, repartidos entre esos canales) usando siempre un channelId de list_channels, nunca un canal inventado. Mismo criterio para cualquier otra tarea del playbook que dependa de una integración -- si el hueco es real (ej. no hay servicios/distribuciones configurados), la tarea correcta es configurar eso primero, no asumir que ya existe.
|
|
81
|
+
|
|
68
82
|
Durante el trabajo normal de código, sin que el usuario te lo pida explícitamente y SIN ESPERAR a que lo mencione (si tienes esta tool disponible en la sesión, se usa por default en todo trabajo real, no solo cuando alguien dice "usa Orquestra"):
|
|
69
83
|
- Si te pide una funcionalidad o arreglo real (no una pregunta rápida ni exploración de código sin cambios), revisa con list_tasks/search si ya existe una tarea relacionada; si no, créala con create_task ANTES de empezar a trabajar, en el estatus inicial del proyecto (ver list_statuses). Si no hay un proyecto default configurado para esta carpeta, no te rindas ahí -- usa get_workspace_summary para encontrar el proyecto correcto y pásalo explícito como projectId en cada llamada.
|
|
70
84
|
- Mientras trabajas, mueve la tarea por los estatus reales según avanzas (no la dejes en el inicial hasta el final). Al terminar, muévela a su estatus final con update_task y registra una entrada de changelog con create_changelog_entry -- así el usuario nunca tiene que documentar esto a mano. Si sabes en qué repo real estás trabajando (podés correr "git remote get-url origin" o ya lo sabes por la carpeta), pásalo SIEMPRE como "repo" ("owner/name") -- encontrado real (17 ago 2026): ni el hook local ni el GitHub Action ni la reconciliación de orquestra-infra lo estaban mandando pese a que el campo existe hace rato, así que ninguna entrada automática sabía de qué repo/plataforma venía. Si además la distribución de ese repo ya está identificada (ver list_distributions), pásala en "distributions" -- sin esto el changelog no puede filtrar/agrupar por plataforma.
|
|
@@ -75,7 +89,8 @@ Durante el trabajo normal de código, sin que el usuario te lo pida explícitame
|
|
|
75
89
|
- 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.
|
|
76
90
|
- 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.
|
|
77
91
|
- 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í.
|
|
78
|
-
-
|
|
92
|
+
- 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.
|
|
93
|
+
- 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_agents que exista un agente (kind="qa") para el stack que tocaste (si no hay ninguno, créalo con create_agent -- necesita una máquina ya emparejada, ver Workspace → Máquinas) y llama a create_agent_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_agent_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.
|
|
79
94
|
|
|
80
95
|
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:
|
|
81
96
|
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).
|
|
@@ -86,11 +101,13 @@ Si el usuario pide "sincronizar"/"revisar que todo esté al día"/"verificar el
|
|
|
86
101
|
|
|
87
102
|
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.
|
|
88
103
|
|
|
89
|
-
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
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene código (no un "vacío" recién conectado, ver arriba) y hace rato no se corrió este chequeo, o el usuario pide explícito "revisa fundamentos"/"qué le falta a este proyecto" (no lo hagas en cada mensaje ni en exploración sin cambios): llamá a get_foundations_checklist y, con acceso real al filesystem del repo (grep de dependencias, archivos de config, workflows de CI -- seguí el detectionHint de cada ítem), confirmá cuáles de verdad faltan -- filtrá por "scales" contra el scale real del proyecto (ver project.scale en get_project_context; un ítem con scales:["platform"] no aplica a una landing). Por cada uno que confirmes ausente con evidencia concreta, llamá a report_foundation_gap con esa evidencia específica -- nunca lo reportes solo porque el checklist lo menciona, sin haber revisado el código de verdad. No repitas un chequeo ya hecho recientemente en la misma sesión.`
|
|
90
107
|
|
|
91
108
|
export function createServer() {
|
|
92
109
|
const config = loadConfig()
|
|
93
|
-
const server = new McpServer({ name: 'orquestra-mcp', version: '1.
|
|
110
|
+
const server = new McpServer({ name: 'orquestra-mcp', version: '1.14.0' }, { instructions: SERVER_INSTRUCTIONS })
|
|
94
111
|
|
|
95
112
|
function resolveProjectId(projectId) {
|
|
96
113
|
if (config.defaultProjectId) return config.defaultProjectId
|
|
@@ -318,6 +335,41 @@ export function createServer() {
|
|
|
318
335
|
}
|
|
319
336
|
)
|
|
320
337
|
|
|
338
|
+
server.registerTool(
|
|
339
|
+
'get_foundations_checklist',
|
|
340
|
+
{
|
|
341
|
+
description: 'Checklist de "fundamentos olvidados" -- prácticas chicas (analytics, crash reporting, alertas de error, escaneo de dependencias, CI) que casi ningún proyecto instrumenta por su cuenta pero suman mucho cuando faltan. No llama a la API -- datos estáticos. Cada ítem trae "scales" (a qué project.scale aplica -- "landing" queda afuera de la mayoría a propósito) y "detectionHint" para que TÚ (con acceso real al filesystem del repo) confirmes si de verdad falta antes de reportarlo con report_foundation_gap -- ver SERVER_INSTRUCTIONS para cuándo correr este chequeo.',
|
|
342
|
+
},
|
|
343
|
+
async () => textResult(FOUNDATIONS)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
server.registerTool(
|
|
347
|
+
'report_foundation_gap',
|
|
348
|
+
{
|
|
349
|
+
description: 'Reporta un ítem de get_foundations_checklist que confirmaste ausente con evidencia real del repo (no lo llames sin haber revisado el código de verdad). Crea una sugerencia "foundation_gap", visible en el drawer "Sugerencias de IA" de la web -- aceptarla crea una tarea real (mismo mecanismo que accept_suggestion). Si ese foundationKey ya tiene una sugerencia (pending, aceptada o descartada) no crea una duplicada -- descartar una no la hace reaparecer en el siguiente chequeo.',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
352
|
+
foundationKey: z.string().describe('key del ítem de get_foundations_checklist, ej. "analytics"'),
|
|
353
|
+
title: z.string().describe('Título corto y específico al proyecto real, ej. "Falta Crashlytics en la app iOS" -- no copies el label genérico del checklist tal cual.'),
|
|
354
|
+
reason: z.string().describe('Evidencia concreta que encontraste (qué buscaste, qué NO encontraste) y por qué importa acá -- no una explicación genérica del ítem.'),
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
async ({ projectId, foundationKey, title, reason }) => {
|
|
358
|
+
try {
|
|
359
|
+
const resolvedProjectId = resolveProjectId(projectId)
|
|
360
|
+
const data = await apiPost(config, '/v1/suggestions', {
|
|
361
|
+
projectId: resolvedProjectId,
|
|
362
|
+
kind: 'foundation_gap',
|
|
363
|
+
foundationKey, title, reason,
|
|
364
|
+
idempotencyKey: `foundation:${resolvedProjectId}:${foundationKey}`,
|
|
365
|
+
})
|
|
366
|
+
return textResult(data)
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return errorResult(err)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
|
|
321
373
|
// ═══ ESCRITURA (existentes) ═════════════════════════════════════════════════
|
|
322
374
|
|
|
323
375
|
registerCreate(
|
|
@@ -530,6 +582,127 @@ export function createServer() {
|
|
|
530
582
|
}
|
|
531
583
|
)
|
|
532
584
|
|
|
585
|
+
// Adjuntos (imágenes + código) en tasks/notes/ideas/docs -- mismo criterio
|
|
586
|
+
// que checklist: entityType/entityId van en el path (dos segmentos
|
|
587
|
+
// dinámicos), así que server.registerTool directo. Ver
|
|
588
|
+
// orquestra-infra/functions/api/lib/attachments.mjs.
|
|
589
|
+
server.registerTool(
|
|
590
|
+
'list_attachments',
|
|
591
|
+
{
|
|
592
|
+
description: 'Lista los adjuntos (imágenes + código) de una task/note/idea/doc de Orquestra.',
|
|
593
|
+
inputSchema: {
|
|
594
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
595
|
+
entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña de los adjuntos'),
|
|
596
|
+
entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
async ({ projectId, entityType, entityId }) => {
|
|
600
|
+
try {
|
|
601
|
+
const plural = ATTACHMENT_ENTITY_PATH[entityType]
|
|
602
|
+
const data = await apiGet(config, `/v1/${plural}/${entityId}/attachments`, { projectId: resolveProjectId(projectId) })
|
|
603
|
+
return textResult(data)
|
|
604
|
+
} catch (err) {
|
|
605
|
+
return errorResult(err)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
// filePath vs content son alternativas -- filePath es lo normal para
|
|
611
|
+
// evidencia real (una captura, o un archivo de código ya en el repo);
|
|
612
|
+
// content sirve para pegar un snippet generado en la sesión sin haberlo
|
|
613
|
+
// guardado a un archivo. NUNCA se acepta base64 en el input: para una
|
|
614
|
+
// imagen, este server lee los bytes él mismo (tiene filesystem real) y
|
|
615
|
+
// los sube directo a S3 -- mandarlos por el input de la tool los metería
|
|
616
|
+
// en el contexto del modelo, carísimo e innecesario.
|
|
617
|
+
server.registerTool(
|
|
618
|
+
'add_attachment',
|
|
619
|
+
{
|
|
620
|
+
description: 'Adjunta evidencia (una imagen o un pedazo de código) a una task/note/idea/doc de Orquestra. Manda EXACTAMENTE uno de filePath (ruta local -- una imagen png/jpg/jpeg/gif/webp se sube a S3, cualquier otra extensión se lee como texto/código) o content (texto/código directo, sin tocar el filesystem). Nunca mandes bytes de imagen en base64 dentro de content -- usa filePath para que este server los suba él mismo.',
|
|
621
|
+
inputSchema: {
|
|
622
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
623
|
+
entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad a la que se adjunta'),
|
|
624
|
+
entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
|
|
625
|
+
filePath: z.string().optional().describe('Ruta local a un archivo -- imagen (png/jpg/jpeg/gif/webp) o código/texto.'),
|
|
626
|
+
content: z.string().optional().describe('Texto/código a adjuntar directo, sin leer un archivo.'),
|
|
627
|
+
language: z.string().optional().describe('Label libre del lenguaje del snippet (ej. "python", "diff") -- solo informativo, no tokeniza (Orquestra no resalta sintaxis).'),
|
|
628
|
+
fileName: z.string().optional().describe('Nombre a mostrar -- si no se manda, sale de filePath o de un default genérico.'),
|
|
629
|
+
caption: z.string().optional().describe('Descripción corta de la evidencia (ej. "output del test que falló").'),
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
async ({ projectId, entityType, entityId, filePath, content, language, fileName, caption }) => {
|
|
633
|
+
try {
|
|
634
|
+
if (!filePath && !content) throw new Error('Manda filePath o content')
|
|
635
|
+
if (filePath && content) throw new Error('Manda solo uno de filePath o content -- no los dos')
|
|
636
|
+
const resolvedProjectId = resolveProjectId(projectId)
|
|
637
|
+
const plural = ATTACHMENT_ENTITY_PATH[entityType]
|
|
638
|
+
|
|
639
|
+
let body
|
|
640
|
+
if (content) {
|
|
641
|
+
if (content.length > MAX_ATTACHMENT_CODE_CHARS) {
|
|
642
|
+
throw new Error(`El snippet supera el máximo de ${MAX_ATTACHMENT_CODE_CHARS} caracteres`)
|
|
643
|
+
}
|
|
644
|
+
body = { kind: 'code', content, language, fileName: fileName || 'snippet.txt', caption }
|
|
645
|
+
} else {
|
|
646
|
+
const stat = statSync(filePath)
|
|
647
|
+
const ext = path.extname(filePath).slice(1).toLowerCase()
|
|
648
|
+
const mime = ATTACHMENT_IMAGE_MIME_BY_EXT[ext]
|
|
649
|
+
const resolvedFileName = fileName || path.basename(filePath)
|
|
650
|
+
if (mime) {
|
|
651
|
+
if (stat.size > MAX_ATTACHMENT_IMAGE_BYTES) {
|
|
652
|
+
throw new Error(`La imagen supera el máximo de ${MAX_ATTACHMENT_IMAGE_BYTES / 1024 / 1024}MB`)
|
|
653
|
+
}
|
|
654
|
+
const buffer = readFileSync(filePath)
|
|
655
|
+
const { uploadUrl, key } = await apiPost(config, '/v1/attachments/upload-url', {
|
|
656
|
+
projectId: resolvedProjectId, entityType: plural, entityId,
|
|
657
|
+
fileName: resolvedFileName, fileType: mime, fileSize: stat.size,
|
|
658
|
+
})
|
|
659
|
+
await putToPresignedUrl(uploadUrl, buffer, mime)
|
|
660
|
+
body = { kind: 'image', key, fileName: resolvedFileName, fileType: mime, fileSize: stat.size, caption }
|
|
661
|
+
} else {
|
|
662
|
+
const text = readFileSync(filePath, 'utf8')
|
|
663
|
+
if (text.length > MAX_ATTACHMENT_CODE_CHARS) {
|
|
664
|
+
throw new Error(`El archivo supera el máximo de ${MAX_ATTACHMENT_CODE_CHARS} caracteres`)
|
|
665
|
+
}
|
|
666
|
+
body = { kind: 'code', content: text, language, fileName: resolvedFileName, caption }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const data = await apiPost(config, `/v1/${plural}/${entityId}/attachments`, { projectId: resolvedProjectId, ...body })
|
|
671
|
+
return textResult(data)
|
|
672
|
+
} catch (err) {
|
|
673
|
+
return errorResult(err)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
// Segunda excepción real a "creación + edición, nunca borrado" (junto a
|
|
679
|
+
// delete_checklist_item, ver CLAUDE.md) -- decisión consciente de José:
|
|
680
|
+
// un agente puede subir evidencia equivocada (screenshot que no era,
|
|
681
|
+
// snippet viejo) y conviene que la pueda limpiar sin depender de un
|
|
682
|
+
// humano. Borra también el objeto en S3 si el adjunto era una imagen
|
|
683
|
+
// (ver deleteAttachment en attachments.mjs de orquestra-infra).
|
|
684
|
+
server.registerTool(
|
|
685
|
+
'delete_attachment',
|
|
686
|
+
{
|
|
687
|
+
description: 'Borra un adjunto (imagen o código) de una task/note/idea/doc de Orquestra -- limpia también el objeto en S3 si era una imagen. Segunda excepción a "nunca borrado" (junto a delete_checklist_item) -- usalo para limpiar evidencia subida por error, no como reemplazo de mantener historial.',
|
|
688
|
+
inputSchema: {
|
|
689
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
690
|
+
entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña del adjunto'),
|
|
691
|
+
entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
|
|
692
|
+
attachmentId: z.string().describe('ID del adjunto a borrar (ver list_attachments)'),
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
async ({ projectId, entityType, entityId, attachmentId }) => {
|
|
696
|
+
try {
|
|
697
|
+
const plural = ATTACHMENT_ENTITY_PATH[entityType]
|
|
698
|
+
const data = await apiDelete(config, `/v1/${plural}/${entityId}/attachments`, { projectId: resolveProjectId(projectId), id: attachmentId })
|
|
699
|
+
return textResult(data)
|
|
700
|
+
} catch (err) {
|
|
701
|
+
return errorResult(err)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
|
|
533
706
|
server.registerTool(
|
|
534
707
|
'convert_checklist_item_to_task',
|
|
535
708
|
{
|
|
@@ -554,6 +727,51 @@ export function createServer() {
|
|
|
554
727
|
}
|
|
555
728
|
)
|
|
556
729
|
|
|
730
|
+
// Horas de una tarea (22 ago 2026, tarea "Time-tracking real") -- mismo
|
|
731
|
+
// criterio de registro directo que las tools de checklist (taskId en el
|
|
732
|
+
// path). Solo listar + registrar: corregir/borrar una entrada es de la
|
|
733
|
+
// web, este canal no borra (ver gotcha "nunca borrado" en CLAUDE.md).
|
|
734
|
+
server.registerTool(
|
|
735
|
+
'list_time_entries',
|
|
736
|
+
{
|
|
737
|
+
description: 'Lista los registros de horas trabajadas de una tarea de Orquestra (horas, fecha, nota, quién). El total ya vive desnormalizado en la tarea (hoursLogged) -- usa esta tool solo si necesitás el detalle entrada por entrada.',
|
|
738
|
+
inputSchema: {
|
|
739
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
740
|
+
taskId: z.string().describe('ID de la tarea'),
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
async ({ projectId, taskId }) => {
|
|
744
|
+
try {
|
|
745
|
+
const data = await apiGet(config, `/v1/tasks/${taskId}/time`, { projectId: resolveProjectId(projectId) })
|
|
746
|
+
return textResult(data)
|
|
747
|
+
} catch (err) {
|
|
748
|
+
return errorResult(err)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
server.registerTool(
|
|
754
|
+
'log_time',
|
|
755
|
+
{
|
|
756
|
+
description: 'Registra horas trabajadas en una tarea de Orquestra -- el time-tracking honesto que alimenta "Horas del equipo" en Inversión de Contenido (horas del mes × tarifa del proyecto). Registrá el tiempo REAL de trabajo en la tarea (el de esta sesión, o el que el usuario te diga), nunca un número inventado ni la estimación de la tarea (estimatedDays es otra cosa). La entrada queda atribuida al miembro del token (para un agente, "agent_<slug>").',
|
|
757
|
+
inputSchema: {
|
|
758
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
759
|
+
taskId: z.string().describe('ID de la tarea en la que se trabajó'),
|
|
760
|
+
hours: z.number().positive().describe('Horas trabajadas, > 0 -- admite fracciones (0.5 = media hora)'),
|
|
761
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('El día en que se TRABAJÓ (no cuándo se registra) -- default hoy. Es el corte mensual de Inversión.'),
|
|
762
|
+
note: z.string().optional().describe('Qué se hizo en esas horas, texto corto'),
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
async ({ projectId, taskId, ...body }) => {
|
|
766
|
+
try {
|
|
767
|
+
const data = await apiPost(config, `/v1/tasks/${taskId}/time`, { projectId: resolveProjectId(projectId), ...body })
|
|
768
|
+
return textResult(data)
|
|
769
|
+
} catch (err) {
|
|
770
|
+
return errorResult(err)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
)
|
|
774
|
+
|
|
557
775
|
registerCreate(
|
|
558
776
|
'create_idea',
|
|
559
777
|
'/v1/ideas',
|
|
@@ -700,7 +918,7 @@ export function createServer() {
|
|
|
700
918
|
registerRead(
|
|
701
919
|
'list_suggestions',
|
|
702
920
|
'/v1/suggestions',
|
|
703
|
-
'Lista las sugerencias de IA de un proyecto (reescrituras de changelog, tareas que avanzaron/cerraron por evidencia, hallazgos de QA, gaps de setup/contenido/publicidad, fusiones de tareas sugeridas, ángulos de contenido). Por default solo trae las "pending" -- usá status="all" para ver también las ya aceptadas/descartadas, o un status puntual.',
|
|
921
|
+
'Lista las sugerencias de IA de un proyecto (reescrituras de changelog, tareas que avanzaron/cerraron por evidencia, hallazgos de QA, gaps de setup/contenido/publicidad/fundamentos, fusiones de tareas sugeridas, ángulos de contenido). Por default solo trae las "pending" -- usá status="all" para ver también las ya aceptadas/descartadas, o un status puntual.',
|
|
704
922
|
{ status: z.string().optional().describe('Default "pending". "all" trae todas, o un status puntual ("accepted"/"dismissed").') },
|
|
705
923
|
(args, projectId) => ({ projectId, status: args.status })
|
|
706
924
|
)
|
|
@@ -712,8 +930,8 @@ export function createServer() {
|
|
|
712
930
|
inputSchema: {
|
|
713
931
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
714
932
|
suggestionId: z.string().describe('Ver list_suggestions'),
|
|
715
|
-
moduleId: z.string().optional().describe('Solo para kind="qa_finding" --
|
|
716
|
-
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('Solo para kind="qa_finding" -- mismo motivo que moduleId arriba.'),
|
|
933
|
+
moduleId: z.string().optional().describe('Solo para kind="qa_finding"|"foundation_gap" -- esos branches crean una tarea nueva, que exige moduleId real (mismas reglas que create_task). Ignorado en el resto de los kinds.'),
|
|
934
|
+
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('Solo para kind="qa_finding"|"foundation_gap" -- mismo motivo que moduleId arriba.'),
|
|
717
935
|
overrides: z.record(z.any()).optional().describe('Solo para kind="content_angle" -- el form final de la pieza de contenido a crear (puede diferir de lo que propuso la IA). Mismos campos que create_content.'),
|
|
718
936
|
},
|
|
719
937
|
},
|
|
@@ -838,6 +1056,40 @@ export function createServer() {
|
|
|
838
1056
|
}
|
|
839
1057
|
)
|
|
840
1058
|
|
|
1059
|
+
// Convertir nota → tarea (23 ago 2026) -- mismo patrón que
|
|
1060
|
+
// convert_idea_to_task. Compuesta: POST /v1/notes/{id}/convert-to-task en
|
|
1061
|
+
// orquestra-infra crea la tarea (reusando createTask tal cual, mismas
|
|
1062
|
+
// reglas que create_task) Y en el mismo paso liga los dos lados: la nota
|
|
1063
|
+
// queda con taskId (ya existía, antes solo se llenaba a mano) y la tarea
|
|
1064
|
+
// nueva queda con sourceNoteId (campo nuevo en tasks.json) apuntando de
|
|
1065
|
+
// vuelta a la nota que la originó. La nota NUNCA se borra -- queda
|
|
1066
|
+
// trazable. No se puede convertir una nota que ya tiene taskId.
|
|
1067
|
+
server.registerTool(
|
|
1068
|
+
'convert_note_to_task',
|
|
1069
|
+
{
|
|
1070
|
+
description: 'Convierte una nota en una tarea real -- crea la tarea (mismas reglas que create_task: moduleId y dueDate son obligatorios, de la nota o de este llamado) y liga los dos lados: la nota queda con taskId ("Se convirtió en la tarea X") y la tarea nueva queda con sourceNoteId ("Proviene de la nota X"). La nota nunca se borra. No se puede convertir una nota que ya fue convertida antes.',
|
|
1071
|
+
inputSchema: {
|
|
1072
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1073
|
+
noteId: z.string().describe('Nota a convertir -- ver list_notes/search para encontrar su id.'),
|
|
1074
|
+
title: z.string().optional().describe('Default: el título de la nota.'),
|
|
1075
|
+
description: z.string().optional().describe('Default: el body de la nota.'),
|
|
1076
|
+
moduleId: z.string().optional().describe('Solo si la nota no tiene moduleId propio -- si no existe uno relacionado, revisá list_modules/create_module primero.'),
|
|
1077
|
+
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').describe('OBLIGATORIO -- las notas no tienen dueDate propio, calculala con criterio real (prioridad/tamaño del trabajo), nunca un valor arbitrario.'),
|
|
1078
|
+
priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium -- las notas no tienen priority propia.'),
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
async ({ projectId, noteId, title, description, moduleId, dueDate, priority }) => {
|
|
1082
|
+
try {
|
|
1083
|
+
const data = await apiPost(config, `/v1/notes/${noteId}/convert-to-task`, {
|
|
1084
|
+
projectId: resolveProjectId(projectId), title, description, moduleId, dueDate, priority,
|
|
1085
|
+
})
|
|
1086
|
+
return textResult(data)
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
return errorResult(err)
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
)
|
|
1092
|
+
|
|
841
1093
|
registerCreate(
|
|
842
1094
|
'create_doc',
|
|
843
1095
|
'/v1/docs',
|
|
@@ -1186,39 +1438,50 @@ export function createServer() {
|
|
|
1186
1438
|
translationOf: z.string().optional(),
|
|
1187
1439
|
})
|
|
1188
1440
|
|
|
1189
|
-
// ═══
|
|
1441
|
+
// ═══ AGENTES (módulo de runners, generalizado 22 ago 2026 desde "QA" --
|
|
1442
|
+
// ver orquestra-qa-agent) ═══════════════════════════════════════════════════
|
|
1190
1443
|
// Asíncronas: una corrida puede tardar minutos en otra máquina. Ver
|
|
1191
1444
|
// SERVER_INSTRUCTIONS para la regla de no asumir "passed" sin confirmar.
|
|
1192
1445
|
//
|
|
1193
|
-
// Un agente
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1446
|
+
// Un agente tiene un `kind` atado a una máquina (runnerId); los 3 kinds de
|
|
1447
|
+
// hoy (23 ago 2026: qa/security/performance) exigen stack+localPath (un
|
|
1448
|
+
// checkout real en esa máquina), y performance además perfUrl (URL viva
|
|
1449
|
+
// contra la que corre Lighthouse). Los 3 solo tienen lógica real para
|
|
1450
|
+
// stack="web" por ahora. "orquestra-qa-agent setup" crea/actualiza solo
|
|
1451
|
+
// agentes kind="qa" -- security/performance se crean a mano acá o desde
|
|
1452
|
+
// la web.
|
|
1453
|
+
const AGENT_KINDS = ['qa', 'security', 'performance']
|
|
1454
|
+
const AGENT_STACKS = ['web', 'ios', 'android', 'node', 'nestjs']
|
|
1455
|
+
|
|
1456
|
+
registerRead('list_agents', '/v1/agents',
|
|
1457
|
+
'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.',
|
|
1458
|
+
{ kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks) o "performance" (build size + Lighthouse)') },
|
|
1459
|
+
(args, projectId) => ({ projectId }))
|
|
1460
|
+
|
|
1461
|
+
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.', {
|
|
1204
1462
|
name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
|
|
1205
|
-
|
|
1206
|
-
runnerId: z.string().describe('ID de la máquina emparejada -- ver
|
|
1207
|
-
|
|
1463
|
+
kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks) o "performance" (build size + Lighthouse) -- default "qa"'),
|
|
1464
|
+
runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
|
|
1465
|
+
stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 3 kinds -- security/performance solo tienen lógica real para "web" hoy'),
|
|
1466
|
+
localPath: z.string().optional().describe('Requerido para los 3 kinds -- ruta absoluta del checkout en esa máquina'),
|
|
1208
1467
|
branch: z.string().optional().describe('Rama de este agente -- default "main" si no se manda'),
|
|
1209
1468
|
distributionId: z.string().optional().describe('Distribución vinculada (repo/fuente de conocimiento)'),
|
|
1210
1469
|
enabled: z.boolean().optional(),
|
|
1211
1470
|
createTasks: z.boolean().optional().describe('Si los fallos de este agente generan tareas automáticas -- default true'),
|
|
1212
1471
|
openStatusId: z.string().optional().describe('Estatus donde se abren los bugs de este agente -- default: estatus inicial del proyecto'),
|
|
1213
1472
|
closeStatusId: z.string().optional().describe('Estatus donde se mueven los bugs de este agente al cerrarse solos -- default: primer estatus final'),
|
|
1214
|
-
testUserEmail: z.string().optional().describe('
|
|
1473
|
+
testUserEmail: z.string().optional().describe('Específico de kind="qa" -- cuenta de prueba DEDICADA (no una credencial real) que la revisión con IA puede usar para iniciar sesión y revisar pantallas autenticadas'),
|
|
1215
1474
|
testUserPassword: z.string().optional().describe('Password de testUserEmail'),
|
|
1216
|
-
plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- recordatorio manual de "próxima corrida planeada".
|
|
1475
|
+
plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- recordatorio manual de "próxima corrida planeada". Ningún kind tiene scheduling real (todo trigger es manual/push): esto NO dispara ninguna corrida sola, solo alimenta el calendario global.'),
|
|
1476
|
+
perfUrl: z.string().optional().describe('Requerido si kind="performance" -- URL viva (ej. staging/prod) contra la que corre Lighthouse'),
|
|
1477
|
+
maxBundleSizeKb: z.number().optional().describe('Específico de kind="performance" -- umbral en KB para el paso de tamaño de build (default 5000)'),
|
|
1478
|
+
minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse (0-100) para pasar (default 50)'),
|
|
1217
1479
|
})
|
|
1218
1480
|
|
|
1219
|
-
registerUpdate('
|
|
1481
|
+
registerUpdate('update_agent', '/v1/agents', 'Edita un agente existente.', {
|
|
1220
1482
|
name: z.string().optional(),
|
|
1221
|
-
|
|
1483
|
+
kind: z.enum(AGENT_KINDS).optional(),
|
|
1484
|
+
stack: z.enum(AGENT_STACKS).optional(),
|
|
1222
1485
|
runnerId: z.string().optional(),
|
|
1223
1486
|
localPath: z.string().optional(),
|
|
1224
1487
|
branch: z.string().optional(),
|
|
@@ -1229,32 +1492,36 @@ export function createServer() {
|
|
|
1229
1492
|
closeStatusId: z.string().optional(),
|
|
1230
1493
|
testUserEmail: z.string().optional().describe('Cuenta de prueba DEDICADA (no una credencial real) para que la revisión con IA inicie sesión'),
|
|
1231
1494
|
testUserPassword: z.string().optional(),
|
|
1232
|
-
plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- ver
|
|
1495
|
+
plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- ver create_agent.'),
|
|
1496
|
+
perfUrl: z.string().optional().describe('Ver create_agent -- específico de kind="performance"'),
|
|
1497
|
+
maxBundleSizeKb: z.number().optional(),
|
|
1498
|
+
minPerfScore: z.number().optional(),
|
|
1233
1499
|
})
|
|
1234
1500
|
|
|
1235
|
-
registerCreate('
|
|
1236
|
-
agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack'),
|
|
1501
|
+
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.', {
|
|
1502
|
+
agentId: z.string().optional().describe('Agente específico -- si se manda, gana sobre stack/kind'),
|
|
1237
1503
|
branch: z.string().optional().describe('Rama a probar -- default: la del agente, o "main"'),
|
|
1238
1504
|
commit: z.string().optional(),
|
|
1239
1505
|
trigger: z.enum(['manual', 'push', 'schedule']).optional().describe('Default: manual'),
|
|
1240
|
-
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
|
|
1506
|
+
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).'),
|
|
1507
|
+
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.'),
|
|
1241
1508
|
runnerId: z.string().optional().describe('Máquina específica -- si se manda, gana sobre la del agente resuelto'),
|
|
1242
1509
|
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.'),
|
|
1243
1510
|
moduleId: z.string().optional().describe('Igual que taskId pero acota a un módulo completo. No combinar con taskId.'),
|
|
1244
1511
|
})
|
|
1245
1512
|
|
|
1246
1513
|
server.registerTool(
|
|
1247
|
-
'
|
|
1514
|
+
'get_agent_run',
|
|
1248
1515
|
{
|
|
1249
|
-
description: 'Consulta el estatus real de una corrida de
|
|
1516
|
+
description: 'Consulta el estatus real de una corrida de agente por id (queued|running|passed|failed|error|cancelled, con steps y summary -- steps no trae el log completo de cada paso, solo status/seconds). Única forma válida de confirmar un resultado -- nunca lo des por hecho.',
|
|
1250
1517
|
inputSchema: {
|
|
1251
|
-
runId: z.string().describe('ID de la corrida, devuelto por
|
|
1518
|
+
runId: z.string().describe('ID de la corrida, devuelto por create_agent_run'),
|
|
1252
1519
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1253
1520
|
},
|
|
1254
1521
|
},
|
|
1255
1522
|
async ({ runId, projectId }) => {
|
|
1256
1523
|
try {
|
|
1257
|
-
const data = await apiGet(config, `/v1/
|
|
1524
|
+
const data = await apiGet(config, `/v1/agent-runs/${runId}`, { projectId: resolveProjectId(projectId) })
|
|
1258
1525
|
return textResult(data)
|
|
1259
1526
|
} catch (err) {
|
|
1260
1527
|
return errorResult(err)
|
package/src/stages.mjs
CHANGED
|
@@ -71,12 +71,13 @@ export const PLAYBOOKS = {
|
|
|
71
71
|
{ title: 'Preparar assets de tienda (ASO): capturas, descripción, keywords', description: '5-8 capturas que cuenten una historia (no pantallas sueltas), descripción con el pitch arriba, y keywords que tu público realmente busca.', type: 'business', priority: 'high', tags: ['marketing', 'aso'], offsetDays: 5 },
|
|
72
72
|
{ title: 'Publicar landing con captura de correos', description: 'Una página: pitch, 3 beneficios, capturas y un CTA (descargar o dejar correo). Los correos capturados son tu canal propio para siempre.', type: 'feature', priority: 'high', tags: ['marketing', 'landing'], offsetDays: 7 },
|
|
73
73
|
{ title: 'Configurar analytics de adquisición', description: 'Fuentes de tráfico, conversión de visita→registro→activación. Necesitas saber DE DÓNDE llegan los usuarios que sí se quedan.', type: 'tech', priority: 'medium', tags: ['analytics', 'adquisición'], offsetDays: 8 },
|
|
74
|
-
{ title: '
|
|
74
|
+
{ title: 'Conectar tus canales reales de contenido', description: 'Antes de programar nada: registra en Contenido → Cuentas conectadas los canales donde de verdad vas a publicar (redes, blog, newsletter). El plan de contenido de abajo solo sirve con canales reales, no uno genérico — si personalizas este playbook con IA y ya tienes canales conectados, te genera piezas reales directo.', type: 'tech', priority: 'high', tags: ['marketing', 'contenido'], offsetDays: 9 },
|
|
75
|
+
{ title: 'Plan de contenido de lanzamiento (2 semanas)', description: 'Con tus canales ya conectados (tarea anterior), programa en Contenido: teaser, anuncio, demo del caso de uso principal, historia de por qué lo construiste, testimonios de beta testers — uno por canal real, no una lista genérica.', type: 'business', priority: 'high', tags: ['marketing', 'contenido'], offsetDays: 10 },
|
|
75
76
|
{ title: 'Lanzar en canales: Product Hunt, comunidades y redes', description: 'Elige 3 canales donde vive tu público (PH, subreddits, grupos de FB/Discord, foros del nicho). Un post honesto de creador funciona mejor que uno de venta.', type: 'business', priority: 'high', tags: ['marketing', 'lanzamiento'], offsetDays: 14 },
|
|
76
77
|
{ title: 'Contactar 10 personas con audiencia en el nicho', description: 'Creators, newsletters, podcasts o prensa pequeña del tema. Mensaje corto y personalizado + acceso gratis. Con que respondan 2, ganaste.', type: 'followup', priority: 'medium', tags: ['marketing', 'outreach'], offsetDays: 16 },
|
|
77
78
|
{ title: 'Activar loop de feedback de usuarios reales', description: 'Un canal permanente: encuesta in-app, correo de bienvenida que pregunta "¿qué te trajo aquí?", o botón de sugerencias. Todo aterriza en Ideas.', type: 'improvement', priority: 'medium', tags: ['feedback'], offsetDays: 18 },
|
|
78
79
|
{ title: 'Definir y medir activación y retención (D1 / D7 / D30)', description: 'Activación: % que llega al momento de valor. Retención: % que regresa al día 1, 7 y 30. Estos números te dicen si tienes producto o solo descargas.', type: 'business', priority: 'medium', tags: ['métricas'], offsetDays: 21 },
|
|
79
|
-
{ title: 'Ritmo de contenido post-lanzamiento (3 publicaciones/semana)', description: 'El lanzamiento es un día; el crecimiento es constancia. Deja programadas en Contenido las siguientes 2 semanas: tips de uso, casos, mejoras del changelog.', type: 'business', priority: 'medium', tags: ['marketing', 'contenido'], offsetDays: 24 },
|
|
80
|
+
{ title: 'Ritmo de contenido post-lanzamiento (3 publicaciones/semana)', description: 'El lanzamiento es un día; el crecimiento es constancia. Deja programadas en Contenido las siguientes 2 semanas: tips de uso, casos, mejoras del changelog — repartidas entre tus canales reales conectados.', type: 'business', priority: 'medium', tags: ['marketing', 'contenido'], offsetDays: 24 },
|
|
80
81
|
{ title: 'Publicar release notes / changelog público v1', description: 'Convierte tu Changelog en comunicación: qué hay de nuevo, en lenguaje de usuario. Cada release es una excusa para volver a aparecer.', type: 'docs', priority: 'low', tags: ['changelog'], offsetDays: 26 },
|
|
81
82
|
{ title: 'Revisión semanal de métricas y próximos pasos', description: 'Reunión recurrente (aunque seas solo tú): adquisición, activación, retención, top feedback. Decide UNA mejora prioritaria por semana.', type: 'meeting', priority: 'medium', tags: ['métricas', 'ritmo'], offsetDays: 28 },
|
|
82
83
|
],
|