orquestra-mcp 1.38.0 → 1.43.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 +8 -5
- package/bin/orquestra-mcp.mjs +1 -1
- package/package.json +2 -2
- package/src/api.mjs +26 -56
- package/src/http.mjs +102 -0
- package/src/server.mjs +251 -4
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 **135 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 —
|
|
@@ -133,6 +133,7 @@ crear/editar, sin `delete_*`:
|
|
|
133
133
|
| Entidad | Tools | Notas |
|
|
134
134
|
|---|---|---|
|
|
135
135
|
| Tareas | `create_task`, `update_task` | `update_task` incluye `scope` — úsalo para resolver desfases de paridad |
|
|
136
|
+
| Verificaciones por plataforma | `list_verifications`, `add_verification`, `update_verification`, `delete_verification`, `verify_with_agent` | Confirmaciones (humanas o de agente) de que la tarea quedó bien en web/iOS/Android/API — NO van en el checklist. La tarea no cierra con verificaciones abiertas y se cierra sola al aprobarse la última; `failed` (con `note`) la regresa a su estatus anterior. También: `verifications` en `create_task`, `waiveVerifications` en `update_task` (solo con confirmación del usuario) y `verificationId` en `create_agent_run` |
|
|
136
137
|
| Ideas | `create_idea` | |
|
|
137
138
|
| Changelog | `create_changelog_entry` | Estilo Keep a Changelog |
|
|
138
139
|
| Notas | `create_note` | |
|
|
@@ -195,13 +196,15 @@ create/update sigue siendo criterio del agente y del humano.
|
|
|
195
196
|
|
|
196
197
|
## Estado
|
|
197
198
|
|
|
198
|
-
|
|
199
|
+
135 tools: lectura (contexto, dashboard, workspace, infra, paridad, standup,
|
|
199
200
|
sync report, setup guide, playbooks de etapa y de plataforma, catálogo de
|
|
200
201
|
skills, search) + `list_*` de cada colección + creación/edición de tareas
|
|
201
|
-
(con checklist, tiempo, adjuntos, merge), ideas (con comentarios), notas,
|
|
202
|
+
(con checklist, verificaciones por plataforma, tiempo, adjuntos, merge), ideas (con comentarios), notas,
|
|
202
203
|
docs, changelog (con corte de versión), módulos, flujo, categorías,
|
|
203
204
|
infraestructura, proyecto, contenido (canales, campañas, contenido,
|
|
204
|
-
Postproxy), agentes, skills y
|
|
205
|
+
Postproxy), agentes, skills, sugerencias y vínculos entre ítems
|
|
206
|
+
(`link_items`/`unlink_items`/`list_relations`) + compuestas (init_project,
|
|
205
207
|
apply_stage_playbook, apply_platform_playbook, install_skill). Sin
|
|
206
|
-
`delete_*` salvo adjuntos
|
|
208
|
+
`delete_*` salvo adjuntos, ítems de checklist, verificaciones y vínculos
|
|
209
|
+
(`unlink_items`). Publicado en npm como
|
|
207
210
|
`orquestra-mcp` (unscoped) -- `npx orquestra-mcp setup`.
|
package/bin/orquestra-mcp.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createServer } from '../src/server.mjs'
|
|
|
4
4
|
import { runSetup } from '../src/setup.mjs'
|
|
5
5
|
import { runVaultSync } from '../src/vaultSync.mjs'
|
|
6
6
|
|
|
7
|
-
const HELP_TEXT = `orquestra-mcp -- Servidor MCP de Orquestra (
|
|
7
|
+
const HELP_TEXT = `orquestra-mcp -- Servidor MCP de Orquestra (132 tools)
|
|
8
8
|
|
|
9
9
|
Uso:
|
|
10
10
|
npx orquestra-mcp Inicia el servidor MCP por stdio -- lo usa tu cliente
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Servidor MCP de Orquestra --
|
|
3
|
+
"version": "1.43.0",
|
|
4
|
+
"description": "Servidor MCP de Orquestra -- 135 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"orquestra-mcp": "bin/orquestra-mcp.mjs"
|
package/src/api.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readConfigFile, resolveProjectForCwd, CONFIG_PATH } from './configFile.mjs'
|
|
2
|
+
import { request } from './http.mjs'
|
|
2
3
|
|
|
3
4
|
const DEFAULT_API_BASE = 'https://api.orquestra.me'
|
|
4
5
|
|
|
@@ -38,12 +39,7 @@ export function loadConfig() {
|
|
|
38
39
|
return { apiBase, apiToken, workspaceId, defaultProjectId }
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export
|
|
42
|
-
constructor(status, message) {
|
|
43
|
-
super(message)
|
|
44
|
-
this.status = status
|
|
45
|
-
}
|
|
46
|
-
}
|
|
42
|
+
export { ApiError } from './http.mjs'
|
|
47
43
|
|
|
48
44
|
function buildUrl(config, path, params) {
|
|
49
45
|
const url = new URL(path, config.apiBase)
|
|
@@ -54,57 +50,29 @@ function buildUrl(config, path, params) {
|
|
|
54
50
|
return url
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
const message = (body && body.error) || (typeof body === 'string' ? body : `HTTP ${res.status}`)
|
|
64
|
-
throw new ApiError(res.status, message)
|
|
65
|
-
}
|
|
66
|
-
return body
|
|
67
|
-
}
|
|
53
|
+
// Todas pasan por request() (http.mjs, ORQ-343): plazo por intento, cancelación de la tool y
|
|
54
|
+
// reintentos acotados -- las lecturas ante fallas transitorias, las escrituras solo ante el
|
|
55
|
+
// throttling del API Gateway (no llegó a la Lambda, no puede duplicar nada).
|
|
56
|
+
const auth = config => ({ Authorization: `Bearer ${config.apiToken}` })
|
|
57
|
+
const jsonHeaders = config => ({ ...auth(config), 'Content-Type': 'application/json' })
|
|
68
58
|
|
|
69
59
|
export async function apiGet(config, path, params = {}) {
|
|
70
|
-
|
|
71
|
-
const res = await fetch(url, {
|
|
72
|
-
headers: { Authorization: `Bearer ${config.apiToken}` },
|
|
73
|
-
})
|
|
74
|
-
return parseResponse(res)
|
|
60
|
+
return request(buildUrl(config, path, params), { headers: auth(config) })
|
|
75
61
|
}
|
|
76
62
|
|
|
77
63
|
export async function apiPost(config, path, { projectId, ...body }) {
|
|
78
|
-
|
|
79
|
-
const res = await fetch(url, {
|
|
80
|
-
method: 'POST',
|
|
81
|
-
headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
|
|
82
|
-
body: JSON.stringify(body),
|
|
83
|
-
})
|
|
84
|
-
return parseResponse(res)
|
|
64
|
+
return request(buildUrl(config, path, { projectId }), { method: 'POST', headers: jsonHeaders(config), body: JSON.stringify(body) })
|
|
85
65
|
}
|
|
86
66
|
|
|
87
67
|
export async function apiPatch(config, path, { projectId, id, ...body }) {
|
|
88
|
-
|
|
89
|
-
const res = await fetch(url, {
|
|
90
|
-
method: 'PATCH',
|
|
91
|
-
headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
|
|
92
|
-
body: JSON.stringify(body),
|
|
93
|
-
})
|
|
94
|
-
return parseResponse(res)
|
|
68
|
+
return request(buildUrl(config, `${path}/${id}`, { projectId }), { method: 'PATCH', headers: jsonHeaders(config), body: JSON.stringify(body) })
|
|
95
69
|
}
|
|
96
70
|
|
|
97
71
|
// Mismo método que apiPatch, pero para un doc singleton SIN id en la ruta
|
|
98
72
|
// (ej. /v1/content-config) -- set_hourly_rate (8 oct 2026, ORQ-247) es el
|
|
99
73
|
// primer caso real.
|
|
100
74
|
export async function apiPatchPath(config, path, { projectId, ...body }) {
|
|
101
|
-
|
|
102
|
-
const res = await fetch(url, {
|
|
103
|
-
method: 'PATCH',
|
|
104
|
-
headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify(body),
|
|
106
|
-
})
|
|
107
|
-
return parseResponse(res)
|
|
75
|
+
return request(buildUrl(config, path, { projectId }), { method: 'PATCH', headers: jsonHeaders(config), body: JSON.stringify(body) })
|
|
108
76
|
}
|
|
109
77
|
|
|
110
78
|
// Único método DELETE del cliente (21 ago 2026, para delete_checklist_item
|
|
@@ -112,23 +80,25 @@ export async function apiPatchPath(config, path, { projectId, ...body }) {
|
|
|
112
80
|
// apiPatch (arma la URL como `${path}/${id}`), sin body -- ningún delete de
|
|
113
81
|
// esta API necesita mandar datos, solo identificar el recurso.
|
|
114
82
|
export async function apiDelete(config, path, { projectId, id }) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
83
|
+
return request(buildUrl(config, `${path}/${id}`, { projectId }), { method: 'DELETE', headers: auth(config) })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// DELETE sobre un recurso singleton SIN id en la ruta (ej. /v1/client-portal,
|
|
87
|
+
// ORQ-332) -- mismo criterio que apiPatchPath.
|
|
88
|
+
export async function apiDeletePath(config, path, { projectId }) {
|
|
89
|
+
return request(buildUrl(config, path, { projectId }), { method: 'DELETE', headers: auth(config) })
|
|
121
90
|
}
|
|
122
91
|
|
|
123
92
|
// PUT crudo a una presigned URL de S3 (createAttachmentUploadUrl en
|
|
124
93
|
// orquestra-infra) -- a diferencia de apiGet/apiPost/etc, este request NO
|
|
125
94
|
// va contra config.apiBase ni lleva el Bearer del MCP: la URL firmada YA
|
|
126
|
-
// es la autorización completa.
|
|
95
|
+
// es la autorización completa. Plazo más largo (sube bytes); un PUT a la
|
|
96
|
+
// misma key es idempotente, pero igual no se reintenta para no duplicar
|
|
97
|
+
// tiempos largos de subida.
|
|
127
98
|
export async function putToPresignedUrl(uploadUrl, buffer, contentType) {
|
|
128
|
-
|
|
129
|
-
method: 'PUT',
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
if (!res.ok) throw new Error(`Falló la subida a S3: HTTP ${res.status}`)
|
|
99
|
+
try {
|
|
100
|
+
await request(uploadUrl, { method: 'PUT', headers: { 'Content-Type': contentType }, body: buffer }, { timeoutMs: 120_000, retries: 0 })
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw new Error(`Falló la subida a S3: ${err.status ? `HTTP ${err.status}` : err.message}`)
|
|
103
|
+
}
|
|
134
104
|
}
|
package/src/http.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// fetch con plazo, cancelación y reintentos acotados (ORQ-343). Antes cada llamada era un fetch
|
|
2
|
+
// sin signal: una API colgada dejaba la tool esperando para siempre y un 503 pasajero se le
|
|
3
|
+
// devolvía al agente como error definitivo.
|
|
4
|
+
//
|
|
5
|
+
// Reglas de reintento (máx. RETRIES extra, espera progresiva con jitter, respeta Retry-After):
|
|
6
|
+
// - Lecturas (GET): error de red, plazo vencido, 429 de throttling, 502/503/504.
|
|
7
|
+
// - Escrituras (POST/PATCH/DELETE): SOLO el 429 de throttling del API Gateway, que garantiza que
|
|
8
|
+
// la petición no llegó a la Lambda. Un plazo vencido o un 5xx en una escritura NO se reintenta:
|
|
9
|
+
// la entidad pudo crearse y reintentar la duplicaría (la API no tiene idempotencia general).
|
|
10
|
+
// - El 429 de la propia API (créditos agotados, tope de regeneraciones...) trae `error` en el
|
|
11
|
+
// body y es definitivo: no se reintenta.
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
13
|
+
|
|
14
|
+
export const TIMEOUT_MS = 30_000
|
|
15
|
+
export const RETRIES = 2
|
|
16
|
+
const BASE_DELAY_MS = 500
|
|
17
|
+
const MAX_DELAY_MS = 5_000
|
|
18
|
+
const RETRY_STATUS_READ = new Set([502, 503, 504])
|
|
19
|
+
|
|
20
|
+
// Signal de cancelación de la tool en curso (lo pone server.mjs por llamada) -- así no hay que
|
|
21
|
+
// pasarlo a mano por las ~140 tools.
|
|
22
|
+
export const cancelContext = new AsyncLocalStorage()
|
|
23
|
+
|
|
24
|
+
export class ApiError extends Error {
|
|
25
|
+
constructor(status, message, { code } = {}) {
|
|
26
|
+
super(message)
|
|
27
|
+
this.status = status
|
|
28
|
+
if (code) this.code = code
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
33
|
+
if (signal?.aborted) return reject(signal.reason)
|
|
34
|
+
const t = setTimeout(resolve, ms)
|
|
35
|
+
signal?.addEventListener('abort', () => { clearTimeout(t); reject(signal.reason) }, { once: true })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// AbortSignal.any no existe en Node 18 (engines >=18).
|
|
39
|
+
function anySignal(signals) {
|
|
40
|
+
const ctrl = new AbortController()
|
|
41
|
+
for (const s of signals.filter(Boolean)) {
|
|
42
|
+
if (s.aborted) { ctrl.abort(s.reason); break }
|
|
43
|
+
s.addEventListener('abort', () => ctrl.abort(s.reason), { once: true })
|
|
44
|
+
}
|
|
45
|
+
return ctrl.signal
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function backoff(attempt, retryAfter) {
|
|
49
|
+
const secs = Number(retryAfter)
|
|
50
|
+
if (Number.isFinite(secs) && secs >= 0) return Math.min(secs * 1000, MAX_DELAY_MS)
|
|
51
|
+
return Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS) * (0.75 + Math.random() * 0.5)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readBody(res) {
|
|
55
|
+
const text = await res.text()
|
|
56
|
+
try { return text ? JSON.parse(text) : null } catch { return text }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isGatewayThrottle = (status, body) => status === 429 && !(body && typeof body === 'object' && body.error)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Hace la petición y devuelve el body parseado (o lanza ApiError). `timeoutMs` es por intento.
|
|
63
|
+
* `signal` (opcional) se suma al de la tool en curso.
|
|
64
|
+
*/
|
|
65
|
+
export async function request(url, init = {}, { timeoutMs = TIMEOUT_MS, retries = RETRIES, signal } = {}) {
|
|
66
|
+
const method = (init.method || 'GET').toUpperCase()
|
|
67
|
+
const isRead = method === 'GET' || method === 'HEAD'
|
|
68
|
+
const cancel = anySignal([signal, cancelContext.getStore()?.signal])
|
|
69
|
+
|
|
70
|
+
for (let attempt = 0; ; attempt++) {
|
|
71
|
+
const last = attempt >= retries
|
|
72
|
+
let res
|
|
73
|
+
try {
|
|
74
|
+
res = await fetch(url, { ...init, signal: anySignal([cancel, AbortSignal.timeout(timeoutMs)]) })
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (cancel.aborted) throw new ApiError(0, 'Cancelado', { code: 'cancelled' })
|
|
77
|
+
const timedOut = err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
78
|
+
if (isRead && !last) { await sleep(backoff(attempt), cancel); continue }
|
|
79
|
+
if (timedOut) {
|
|
80
|
+
throw new ApiError(0, isRead
|
|
81
|
+
? `La API no respondió en ${timeoutMs / 1000} s (${retries + 1} intentos)`
|
|
82
|
+
: `La API no respondió en ${timeoutMs / 1000} s. La escritura pudo haberse aplicado: revisa con la tool list_*/get_* correspondiente antes de reintentar, para no duplicarla.`,
|
|
83
|
+
{ code: 'timeout' })
|
|
84
|
+
}
|
|
85
|
+
throw new ApiError(0, isRead
|
|
86
|
+
? `No se pudo conectar con la API: ${err?.cause?.code || err?.message}`
|
|
87
|
+
: `Se perdió la conexión con la API (${err?.cause?.code || err?.message}). La escritura pudo haberse aplicado: revisa antes de reintentar.`,
|
|
88
|
+
{ code: 'network' })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const body = await readBody(res)
|
|
92
|
+
if (res.ok) return body
|
|
93
|
+
|
|
94
|
+
const retryable = isGatewayThrottle(res.status, body) || (isRead && RETRY_STATUS_READ.has(res.status))
|
|
95
|
+
if (retryable && !last) {
|
|
96
|
+
await sleep(backoff(attempt, res.headers.get('retry-after')), cancel)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const message = (body && body.error) || (body && body.message) || (typeof body === 'string' && body) || `HTTP ${res.status}`
|
|
100
|
+
throw new ApiError(res.status, message)
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/server.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'node:path'
|
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
5
5
|
import { z } from 'zod'
|
|
6
|
-
import { loadConfig, apiGet, apiPost, apiPatch, apiPatchPath, apiDelete, putToPresignedUrl, ApiError } from './api.mjs'
|
|
6
|
+
import { loadConfig, apiGet, apiPost, apiPatch, apiPatchPath, apiDelete, apiDeletePath, putToPresignedUrl, ApiError } from './api.mjs'
|
|
7
|
+
import { cancelContext } from './http.mjs'
|
|
7
8
|
import { STAGES, PLAYBOOKS } from './stages.mjs'
|
|
8
9
|
import { FOUNDATIONS } from './foundations.mjs'
|
|
9
10
|
import { PLATFORM_PLAYBOOKS, TRACKING_TOOLS } from './platformPlaybooks.mjs'
|
|
@@ -31,6 +32,22 @@ const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'de
|
|
|
31
32
|
const ACTIVITY_TYPES = ['meeting', 'call', 'followup']
|
|
32
33
|
const ACTIVITY_STATUSES = ['scheduled', 'done', 'cancelled']
|
|
33
34
|
const TASK_PRIORITIES = ['high', 'medium', 'low']
|
|
35
|
+
// Relaciones (ORQ-326, ORQ-347) -- misma lista que orquestra-infra/schema/enums/RelationItemType.json.
|
|
36
|
+
const RELATION_TYPES = ['task', 'activity', 'note', 'doc', 'idea', 'risk', 'content', 'changelog', 'distribution', 'service']
|
|
37
|
+
// `relatedTo` en los create_* (ORQ-347): de dónde sale lo que se crea. El server valida el destino
|
|
38
|
+
// antes de crear y deja el vínculo hecho (lo ve list_relations desde los dos lados).
|
|
39
|
+
const RELATED_TO = z.object({
|
|
40
|
+
type: z.enum(RELATION_TYPES).describe('"task" o "activity" (reunión/llamada/seguimiento) normalmente'),
|
|
41
|
+
id: z.string().describe('id del ítem de origen'),
|
|
42
|
+
}).optional().describe('De dónde sale esto (ej. el riesgo/nota/reunión/idea que surgió trabajando en una tarea: {type:"task", id}). Queda vinculado en "Relacionado" de los dos lados -- úsalo en vez de escribir "sale de ORQ-123" en el texto.')
|
|
43
|
+
// Verificaciones por plataforma de una tarea (ORQ-319) -- ver
|
|
44
|
+
// orquestra-infra/functions/api/lib/verifications.mjs para las reglas reales.
|
|
45
|
+
const VERIFICATION_PLATFORMS = ['web', 'ios', 'android', 'api', 'other']
|
|
46
|
+
const VERIFICATION_MODES = ['human', 'agent', 'either']
|
|
47
|
+
const VERIFICATION_STATUSES = ['pending', 'passed', 'failed', 'waived']
|
|
48
|
+
const VERIFICATION_PLATFORM_DESC = 'Plataforma donde se confirma: "web" | "ios" | "android" | "api" | "other". Una verificación por plataforma.'
|
|
49
|
+
const VERIFICATION_INSTRUCTIONS_DESC = 'Pasos CONCRETOS para confirmar que quedó bien (qué abrir, qué hacer, qué se espera ver) -- ej. "En un iPhone físico con la build de TestFlight: abre Tareas, desliza una tarea a la izquierda y confirma que aparece Archivar". Nada genérico tipo "probar que funcione".'
|
|
50
|
+
const VERIFICATION_MODE_DESC = '"human" (default: solo una persona puede confirmarla, ej. dispositivo físico), "agent" (la puede confirmar un agente con revisión con IA) o "either" (cualquiera de los dos).'
|
|
34
51
|
const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
|
|
35
52
|
// 'postmortem' faltaba acá (gap preexistente, encontrado 12 ago 2026) --
|
|
36
53
|
// mismo fix del lado de write.mjs en orquestra-infra.
|
|
@@ -100,10 +117,13 @@ Analítica y anuncios en un sitio web: antes de instalar o tocar Google Analytic
|
|
|
100
117
|
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"):
|
|
101
118
|
- 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.
|
|
102
119
|
- Orquestra es el ÚNICO lugar donde se planea y se lleva el avance del trabajo: el plan de pasos va como checklist de la tarea (add_checklist_item) y se marca con update_checklist_item conforme terminas cada uno. No lleves una lista de pendientes paralela en el chat, en archivos temporales ni en la herramienta de tareas de tu propio cliente -- se pierde al cerrar la sesión y el usuario no la ve. Si retomas trabajo en una sesión nueva, parte de la tarea y sus ítems pendientes (list_tasks + list_checklist_items), no de cero. Excepción: un cambio de un solo paso (renombrar algo, responder una pregunta) no necesita tarea.
|
|
103
|
-
-
|
|
120
|
+
- Verificaciones por plataforma (distintas del checklist): cuando terminas código que necesita confirmación humana o en otra plataforma (probar en un iPhone/Android físico, revisar en la web en producción, pegarle al endpoint real desplegado...), NO lo agregues como ítem de checklist ni dejes la tarea abierta sin más. Registra UNA verificación por plataforma con instrucciones concretas (qué abrir, qué hacer, qué se espera ver) -- con add_verification, o con verifications en create_task si ya lo sabes al crearla -- y mueve la tarea a "En revisión" con un comment que diga qué falta confirmar. La tarea no se puede cerrar con verificaciones abiertas y se cierra SOLA cuando todas quedan aprobadas (update_verification status "passed"; si una falla, "failed" con note y la tarea regresa a su estatus anterior). El checklist es para trabajo; las verificaciones, para confirmar. Si la verificación tiene mode "agent"/"either" y hay un agente con revisión con IA, puedes encolarla con verify_with_agent. Nunca marques passed sin evidencia real de que se probó, y no uses waiveVerifications (ni status "waived") salvo que el usuario lo confirme explícito.
|
|
121
|
+
- 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 (o a "En revisión" si quedaron verificaciones por plataforma abiertas, ver arriba) 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.
|
|
104
122
|
- Todo cambio de statusId vía update_task valida la transición (nextIds) y queda en el historial de la tarea -- pero ese historial no dice nada por sí solo. Manda SIEMPRE el parámetro "comment" cuando cambies statusId, explicando qué hiciste o por qué -- sin esto, el usuario ve que la tarea cambió pero no sabe por qué ni qué se hizo de verdad (encontrado real: quedaba invisible, a diferencia de cuando un humano mueve la tarjeta a mano en el kanban).
|
|
105
123
|
- Conocimiento que sirve más allá de un proyecto (guías, procesos, decisiones transversales) va en la base del workspace: revisa list_knowledge antes de documentarlo de nuevo, créalo con create_knowledge o súbelo con promote_to_knowledge; lo de un solo proyecto sigue en notas/docs.
|
|
106
124
|
- Reuniones, llamadas y seguimientos NO son tareas: son actividades (no se entregan, se programan y se registran). Usa log_activity para agendarlas o registrarlas, update_activity para marcarlas hechas con su resultado (outcome) y siguiente paso (nextStep), y list_activities para ver la agenda/bitácora -- nunca create_task con esos tipos. Si del siguiente paso sale trabajo real, eso sí es una tarea (create_task normal, con módulo y fecha).
|
|
125
|
+
- Para dejar constancia de que una nota, doc, idea, tarea, riesgo, pieza de contenido, entrada de changelog, distribución o servicio es DE otra cosa del proyecto -- "esta nota es de la reunión X", "esta tarea sale de este doc" --, vincúlalas con link_items en vez de meter la referencia como texto en la descripción/cuerpo; list_relations muestra todo lo relacionado de un ítem.
|
|
126
|
+
- Si mientras trabajas en una tarea creas algo que sale de ella (un riesgo que detectaste, una reunión, una nota, una idea, una entrada de changelog...), manda relatedTo: {type:"task", id} en el create_* para que quede ligado desde el principio. Si un paso del checklist depende de otra cosa (otra tarea, un doc, un riesgo), vincúlalo desde ese ítem con link_items + checklistItemId; un folio suelto en el texto del ítem ("requiere ORQ-75") el server lo vincula solo.
|
|
107
127
|
- Antes de crear un módulo/estatus/categoría/servicio/entorno/distribución, revisa con el list_* correspondiente que no exista ya uno con ese nombre.
|
|
108
128
|
- 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.
|
|
109
129
|
- 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.
|
|
@@ -137,12 +157,23 @@ const SCOPE_EVIDENCE = {
|
|
|
137
157
|
doneSource: z.enum(['manual', 'github', 'sibling']).optional(),
|
|
138
158
|
commitSha: z.string().optional().describe('commit que respalda que ya está hecho en esa plataforma'),
|
|
139
159
|
commitUrl: z.string().optional(),
|
|
160
|
+
notApplicable: z.boolean().optional().describe('true = esta plataforma NO aplica para la tarea (cuenta como resuelta en paridad). Solo con el motivo y cuando el usuario lo decidió -- nunca para tapar trabajo pendiente.'),
|
|
161
|
+
notApplicableReason: z.string().optional().describe('Motivo de "no aplica" (obligatorio si notApplicable es true)'),
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
export function createServer() {
|
|
143
165
|
const config = loadConfig()
|
|
144
166
|
const server = new McpServer({ name: 'orquestra-mcp', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS })
|
|
145
167
|
|
|
168
|
+
// Cancelación (ORQ-343): el SDK pasa `extra.signal` (último argumento del handler) y lo aborta
|
|
169
|
+
// si el cliente cancela la tool. Se publica en cancelContext para que request() lo use sin
|
|
170
|
+
// tener que pasarlo a mano por cada tool.
|
|
171
|
+
const registerTool = server.registerTool.bind(server)
|
|
172
|
+
server.registerTool = (name, def, handler) => registerTool(name, def, (...args) => {
|
|
173
|
+
const extra = args[args.length - 1]
|
|
174
|
+
return cancelContext.run({ signal: extra?.signal }, () => handler(...args))
|
|
175
|
+
})
|
|
176
|
+
|
|
146
177
|
function resolveProjectId(projectId) {
|
|
147
178
|
if (config.defaultProjectId) return config.defaultProjectId
|
|
148
179
|
if (!projectId) {
|
|
@@ -246,7 +277,7 @@ export function createServer() {
|
|
|
246
277
|
registerRead(
|
|
247
278
|
'search',
|
|
248
279
|
'/v1/search',
|
|
249
|
-
'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra.
|
|
280
|
+
'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra. También por folio ("ORQ-123", la exacta sale primero) y sin importar acentos. Primero lo que coincide en el folio o el título, después en el cuerpo; tope de 30 resultados: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
|
|
250
281
|
{
|
|
251
282
|
q: z.string().describe('Texto a buscar'),
|
|
252
283
|
types: z.array(z.enum(['task', 'activity', 'note', 'idea', 'doc', 'knowledge'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en todos. knowledge = solo la base vinculada a este proyecto.'),
|
|
@@ -613,6 +644,7 @@ export function createServer() {
|
|
|
613
644
|
'/v1/tasks',
|
|
614
645
|
'Crea una tarea en un proyecto de Orquestra. moduleId y dueDate son OBLIGATORIOS -- revisa list_modules primero (si ninguno aplica, creá uno con create_module antes de crear la tarea, nunca la dejes sin agrupar); dueDate se calcula con criterio real (prioridad/tamaño del trabajo), nunca un valor arbitrario. Una reunión, llamada o seguimiento NO es tarea: usa log_activity (type "business" sí es tarea: entregable no-código).',
|
|
615
646
|
{
|
|
647
|
+
relatedTo: RELATED_TO,
|
|
616
648
|
title: z.string().describe('Título de la tarea'),
|
|
617
649
|
description: z.string().optional(),
|
|
618
650
|
type: z.enum(TASK_TYPES).optional().describe('Default: feature'),
|
|
@@ -644,13 +676,18 @@ export function createServer() {
|
|
|
644
676
|
resolvedCommitMessage: z.string().optional(),
|
|
645
677
|
resolvedCommitUrl: z.string().optional(),
|
|
646
678
|
resolvedCommitAt: z.number().optional().describe('Timestamp en ms del commit resolutor.'),
|
|
679
|
+
verifications: z.array(z.object({
|
|
680
|
+
platform: z.enum(VERIFICATION_PLATFORMS).describe(VERIFICATION_PLATFORM_DESC),
|
|
681
|
+
instructions: z.string().min(1).describe(VERIFICATION_INSTRUCTIONS_DESC),
|
|
682
|
+
mode: z.enum(VERIFICATION_MODES).optional().describe(VERIFICATION_MODE_DESC),
|
|
683
|
+
})).optional().describe('Verificaciones por plataforma desde la creación (mismo efecto que add_verification después). Son confirmaciones humanas o en otra plataforma ("probar en iPhone físico", "revisar en la web en producción"), NO pasos de trabajo -- esos van en el checklist. La tarea no se puede cerrar mientras alguna siga abierta, y se cierra sola cuando todas quedan aprobadas.'),
|
|
647
684
|
}
|
|
648
685
|
)
|
|
649
686
|
|
|
650
687
|
registerUpdate(
|
|
651
688
|
'update_task',
|
|
652
689
|
'/v1/tasks',
|
|
653
|
-
'Edita una tarea existente (incluido su scope multiplataforma, para resolver desfases de paridad). Si cambias statusId, el server valida que la transición sea válida (nextIds del estatus actual -- ver list_statuses) y deja rastro en el historial de la tarea (quién/cuándo/de qué a qué) -- pásale SIEMPRE `comment` cuando cambies statusId para que ese rastro también diga QUÉ hiciste o POR QUÉ, no solo el cambio de id. No deja pasar a un estatus final (isFinal) con ítems del checklist sin terminar (400 "Quedan X de Y..."): marca primero los ítems que de verdad hiciste con update_checklist_item; usa completeChecklist:true solo si el usuario confirma que lo pendiente ya no aplica o ya está hecho.',
|
|
690
|
+
'Edita una tarea existente (incluido su scope multiplataforma, para resolver desfases de paridad). Si cambias statusId, el server valida que la transición sea válida (nextIds del estatus actual -- ver list_statuses) y deja rastro en el historial de la tarea (quién/cuándo/de qué a qué) -- pásale SIEMPRE `comment` cuando cambies statusId para que ese rastro también diga QUÉ hiciste o POR QUÉ, no solo el cambio de id. No deja pasar a un estatus final (isFinal) con ítems del checklist sin terminar (400 "Quedan X de Y..."): marca primero los ítems que de verdad hiciste con update_checklist_item; usa completeChecklist:true solo si el usuario confirma que lo pendiente ya no aplica o ya está hecho. Tampoco deja cerrar con verificaciones por plataforma abiertas (pending/failed, ver list_verifications): no cierres a mano -- deja la tarea en "En revisión" y se cierra sola cuando todas quedan aprobadas; waiveVerifications:true solo con confirmación explícita del usuario.',
|
|
654
691
|
{
|
|
655
692
|
title: z.string().optional(),
|
|
656
693
|
description: z.string().optional(),
|
|
@@ -658,6 +695,7 @@ export function createServer() {
|
|
|
658
695
|
priority: z.enum(TASK_PRIORITIES).optional(),
|
|
659
696
|
statusId: z.string().optional(),
|
|
660
697
|
completeChecklist: z.boolean().optional().describe('Solo junto con un statusId final: marca como hechos los ítems pendientes del checklist y cierra (queda comentado en la tarea). Sin esto, cerrar con checklist pendiente responde 400.'),
|
|
698
|
+
waiveVerifications: z.boolean().optional().describe('Solo junto con un statusId final: marca como omitidas ("waived", nota "Omitida al cerrar la tarea") las verificaciones abiertas y cierra (queda comentado en la tarea). Sin esto, cerrar con verificaciones pendientes o fallidas responde 400. NO lo uses salvo que el usuario confirme explícito que esas verificaciones ya no aplican -- lo normal es dejar la tarea en "En revisión" y que se cierre sola al aprobarse.'),
|
|
661
699
|
categoryId: z.string().optional(),
|
|
662
700
|
moduleId: z.string().optional(),
|
|
663
701
|
dueDate: z.string().optional().describe('YYYY-MM-DD'),
|
|
@@ -724,6 +762,7 @@ export function createServer() {
|
|
|
724
762
|
time: z.string().regex(/^\d{2}:\d{2}$/, 'Formato esperado: HH:MM').optional().describe('Hora local (HH:MM, 24 h), si se sabe.'),
|
|
725
763
|
activityStatus: z.enum(ACTIVITY_STATUSES).optional().describe('Default "scheduled" (programada). "done" para registrar una que ya pasó.'),
|
|
726
764
|
...ACTIVITY_FIELDS_SCHEMA,
|
|
765
|
+
relatedTo: RELATED_TO,
|
|
727
766
|
},
|
|
728
767
|
},
|
|
729
768
|
async ({ projectId, date, time, ...body }) => {
|
|
@@ -881,6 +920,99 @@ export function createServer() {
|
|
|
881
920
|
(args, projectId) => ({ projectId, status: args.status, type: args.type, moduleId: args.moduleId, from: args.from, to: args.to, limit: args.limit })
|
|
882
921
|
)
|
|
883
922
|
|
|
923
|
+
// Relaciones (ORQ-326): vínculo manual NO dirigido entre dos cosas del proyecto. El server ordena el par y
|
|
924
|
+
// el id es determinístico (vincular dos veces no duplica). Ver orquestra-infra/functions/api/lib/relations.mjs.
|
|
925
|
+
registerRead(
|
|
926
|
+
'list_relations',
|
|
927
|
+
'/v1/relations',
|
|
928
|
+
'Sección "Relacionado" de una tarea, actividad (reunión/llamada/seguimiento), nota, doc, idea, riesgo, contenido, entrada de changelog, distribución o servicio: junta los vínculos manuales (link_items, source "manual", con relationId para unlink_items; source "checklist" + checklistItemId si sale de un ítem del checklist de la tarea) Y los que ya salen de campos existentes (nota de la tarea/minuta de la reunión, nota relacionada, ADR publicado, tarea que proviene de una nota, siguiente paso de una reunión, tareas ligadas a una idea -- source = esa etiqueta, relationId null, no se pueden quitar desde acá). Sin duplicados (el manual gana) y sin extremos borrados. No incluye menciones [[...]] -- esas van en get_backlinks.',
|
|
929
|
+
{
|
|
930
|
+
type: z.enum(RELATION_TYPES).describe('"activity" para reunión/llamada/seguimiento, "task" para cualquier otra tarea -- el server rechaza el tipo equivocado'),
|
|
931
|
+
id: z.string().describe('id del ítem (de list_tasks/list_activities/list_notes/list_docs/list_ideas)'),
|
|
932
|
+
},
|
|
933
|
+
(args, projectId) => ({ projectId, type: args.type, id: args.id })
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
registerCreate(
|
|
937
|
+
'link_items',
|
|
938
|
+
'/v1/relations',
|
|
939
|
+
'Vincula a mano dos cosas del mismo proyecto (tarea, actividad, nota, doc, idea, riesgo, contenido, entrada de changelog, distribución o servicio) -- ej. "esta nota es de la reunión X", "esta tarea sale de este doc", "este riesgo es del backend de prod". Con checklistItemId + taskId el vínculo sale de un ÍTEM del checklist de esa tarea (ej. "este paso requiere ORQ-75"): se ve bajo el ítem y se borra con él. El vínculo no tiene dirección (el orden a/b da igual, el server lo ordena) y es idempotente: vincular dos veces lo mismo no duplica (responde alreadyLinked). Saca los ids de list_tasks, list_activities, list_notes, list_docs, list_ideas, list_risks, list_content, list_changelog, list_distributions y list_services; una reunión/llamada/seguimiento es "activity", no "task" (el server valida el tipo real y que ambos existan). No se puede vincular algo consigo mismo. Úsalo en vez de escribir "ver reunión X" en el texto.',
|
|
940
|
+
{
|
|
941
|
+
aType: z.enum(RELATION_TYPES).describe('Tipo del primer ítem'),
|
|
942
|
+
aId: z.string().describe('id del primer ítem'),
|
|
943
|
+
bType: z.enum(RELATION_TYPES).describe('Tipo del segundo ítem'),
|
|
944
|
+
bId: z.string().describe('id del segundo ítem'),
|
|
945
|
+
checklistItemId: z.string().optional().describe('id del ítem del checklist (list_checklist_items) del que sale el vínculo -- requiere taskId'),
|
|
946
|
+
taskId: z.string().optional().describe('Con checklistItemId: la tarea dueña del ítem, que tiene que ser uno de los dos extremos (aType "task")'),
|
|
947
|
+
}
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
server.registerTool(
|
|
951
|
+
'unlink_items',
|
|
952
|
+
{
|
|
953
|
+
description: 'Quita un vínculo manual entre dos ítems (creado con link_items). relationId sale de list_relations (solo los de source "manual" tienen uno; los derivados de campos existentes no se quitan acá). No borra ninguno de los dos ítems. 404 si el vínculo ya no existe.',
|
|
954
|
+
inputSchema: {
|
|
955
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
956
|
+
relationId: z.string().describe('relationId de list_relations, ej. "activity_abc__note_xyz"'),
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
async ({ projectId, relationId }) => {
|
|
960
|
+
try {
|
|
961
|
+
return textResult(await apiDelete(config, '/v1/relations', { projectId: resolveProjectId(projectId), id: relationId }))
|
|
962
|
+
} catch (err) {
|
|
963
|
+
return errorResult(err)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
// Portal del cliente (ORQ-332, 17 oct 2026): UN link por proyecto, sin cuenta,
|
|
969
|
+
// que junta avance, aprobaciones de Contenido, docs públicos y novedades. Ver
|
|
970
|
+
// orquestra-infra/functions/api/lib/clientPortal.mjs. Ruta singleton sin id.
|
|
971
|
+
server.registerTool(
|
|
972
|
+
'get_client_portal_link',
|
|
973
|
+
{
|
|
974
|
+
description: 'Devuelve el link del portal del cliente del proyecto (una página pública, sin cuenta, para el cliente de un freelancer: avance, piezas de Contenido en revisión para aprobar o pedir cambios, lo ya aprobado, docs con visibilidad pública y novedades). `link: null` si no hay uno activo. El link es una credencial: compártelo solo con el cliente del proyecto.',
|
|
975
|
+
inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC) },
|
|
976
|
+
},
|
|
977
|
+
async ({ projectId }) => {
|
|
978
|
+
try {
|
|
979
|
+
return textResult(await apiGet(config, '/v1/client-portal', { projectId: resolveProjectId(projectId) }))
|
|
980
|
+
} catch (err) {
|
|
981
|
+
return errorResult(err)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
server.registerTool(
|
|
987
|
+
'create_client_portal_link',
|
|
988
|
+
{
|
|
989
|
+
description: 'Genera el link del portal del cliente. Si ya había uno, lo REEMPLAZA: el anterior deja de funcionar para quien lo tenga -- confirma con el usuario antes de regenerar uno existente (revisa get_client_portal_link). Qué ve el cliente: avance, Contenido en estatus "review" (puede aprobar o pedir cambios), últimas 10 aprobadas, docs con visibility "public" y novedades del changelog. Nunca tareas, miembros ni notas.',
|
|
990
|
+
inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC) },
|
|
991
|
+
},
|
|
992
|
+
async ({ projectId }) => {
|
|
993
|
+
try {
|
|
994
|
+
return textResult(await apiPost(config, '/v1/client-portal', { projectId: resolveProjectId(projectId) }))
|
|
995
|
+
} catch (err) {
|
|
996
|
+
return errorResult(err)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
server.registerTool(
|
|
1002
|
+
'revoke_client_portal_link',
|
|
1003
|
+
{
|
|
1004
|
+
description: 'Revoca el link del portal del cliente: el cliente deja de poder abrirlo (por ejemplo, al terminar el proyecto o si el link se filtró). No borra nada del proyecto. Para dar acceso de nuevo, create_client_portal_link genera uno distinto.',
|
|
1005
|
+
inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC) },
|
|
1006
|
+
},
|
|
1007
|
+
async ({ projectId }) => {
|
|
1008
|
+
try {
|
|
1009
|
+
return textResult(await apiDeletePath(config, '/v1/client-portal', { projectId: resolveProjectId(projectId) }))
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
return errorResult(err)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
)
|
|
1015
|
+
|
|
884
1016
|
// Fusionar tareas (20 ago 2026) -- registradas directo con
|
|
885
1017
|
// server.registerTool (no registerCreate/registerUpdate) porque son
|
|
886
1018
|
// POST a `${path}/${id}/merge|unmerge`, no un CRUD estándar. Fusión
|
|
@@ -1404,6 +1536,112 @@ export function createServer() {
|
|
|
1404
1536
|
}
|
|
1405
1537
|
)
|
|
1406
1538
|
|
|
1539
|
+
// Verificaciones por plataforma de una tarea (ORQ-319). Distintas del
|
|
1540
|
+
// checklist: el checklist es TRABAJO de la tarea; una verificación es la
|
|
1541
|
+
// CONFIRMACIÓN (humana o de un agente) de que quedó bien en una plataforma.
|
|
1542
|
+
// Mismo criterio que el checklist -- dos ids dinámicos en la ruta, así que
|
|
1543
|
+
// server.registerTool directo. Reglas reales (gate de cierre, cierre
|
|
1544
|
+
// automático, regreso de estatus al fallar) viven en
|
|
1545
|
+
// orquestra-infra/functions/api/lib/verifications.mjs.
|
|
1546
|
+
registerRead(
|
|
1547
|
+
'list_verifications',
|
|
1548
|
+
'/v1/verifications',
|
|
1549
|
+
'Lista las verificaciones por plataforma del proyecto (con taskId/shortCode/taskTitle/taskStatusId/assignedTo de su tarea). Por default solo las abiertas (pending + failed) de tareas que no están cerradas -- úsala para ver qué falta confirmar en cada plataforma (ej. "qué tengo que probar en iOS").',
|
|
1550
|
+
{
|
|
1551
|
+
status: z.enum(['open', 'pending', 'failed', 'passed', 'waived', 'all']).optional().describe('Default "open" (pending + failed, excluye tareas en estatus final/cancelado/archivado). "all" trae todo, incluidas tareas cerradas.'),
|
|
1552
|
+
platform: z.enum(VERIFICATION_PLATFORMS).optional().describe('Filtra por plataforma'),
|
|
1553
|
+
},
|
|
1554
|
+
(args, projectId) => ({ projectId, status: args.status, platform: args.platform })
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
server.registerTool(
|
|
1558
|
+
'add_verification',
|
|
1559
|
+
{
|
|
1560
|
+
description: 'Registra una verificación por plataforma en una tarea: algo que una persona (o un agente) tiene que CONFIRMAR después de que el código ya está hecho -- probar en un iPhone/Android físico, revisar en la web en producción, pegarle al endpoint real, etc. NO es un ítem de checklist (el checklist es trabajo por hacer). Una por plataforma, con instrucciones concretas. Después mueve la tarea a "En revisión": no se puede cerrar mientras haya verificaciones abiertas, y se cierra sola cuando todas quedan aprobadas.',
|
|
1561
|
+
inputSchema: {
|
|
1562
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1563
|
+
taskId: z.string().describe('ID de la tarea'),
|
|
1564
|
+
platform: z.enum(VERIFICATION_PLATFORMS).describe(VERIFICATION_PLATFORM_DESC),
|
|
1565
|
+
instructions: z.string().min(1).describe(VERIFICATION_INSTRUCTIONS_DESC),
|
|
1566
|
+
mode: z.enum(VERIFICATION_MODES).optional().describe(VERIFICATION_MODE_DESC),
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
async ({ projectId, taskId, ...body }) => {
|
|
1570
|
+
try {
|
|
1571
|
+
const data = await apiPost(config, `/v1/tasks/${taskId}/verifications`, { projectId: resolveProjectId(projectId), ...body })
|
|
1572
|
+
return textResult(data)
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
return errorResult(err)
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
server.registerTool(
|
|
1580
|
+
'update_verification',
|
|
1581
|
+
{
|
|
1582
|
+
description: 'Edita o resuelve una verificación de una tarea. status: "passed" (aprobada), "failed" (falló -- exige note con lo que se vio), "waived" (omitida -- exige note con el motivo; solo si el usuario confirma que ya no aplica) o "pending" (re-verificar: limpia nota y resolución). Al resolver (passed/waived) la ÚLTIMA abierta, el server cierra la tarea solo si su estatus actual tiene un estatus final en sus transiciones y el checklist no tiene pendientes (la respuesta trae autoClosed:true); si no puede, comenta y avisa al responsable. Al marcar "failed", la tarea regresa a su estatus anterior (típico: En revisión → En progreso; ver movedBackTo en la respuesta) y se notifica al responsable. Marca passed solo con evidencia real de que se probó, nunca por suponer que funciona.',
|
|
1583
|
+
inputSchema: {
|
|
1584
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1585
|
+
taskId: z.string().describe('ID de la tarea'),
|
|
1586
|
+
verificationId: z.string().describe('ID de la verificación (ver list_verifications o la tarea)'),
|
|
1587
|
+
status: z.enum(VERIFICATION_STATUSES).optional().describe('"pending" | "passed" | "failed" | "waived". failed y waived exigen note.'),
|
|
1588
|
+
note: z.string().optional().describe('Obligatoria con status "failed" (qué falló) o "waived" (por qué se omite).'),
|
|
1589
|
+
instructions: z.string().optional().describe(VERIFICATION_INSTRUCTIONS_DESC),
|
|
1590
|
+
platform: z.enum(VERIFICATION_PLATFORMS).optional(),
|
|
1591
|
+
mode: z.enum(VERIFICATION_MODES).optional().describe(VERIFICATION_MODE_DESC),
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
async ({ projectId, taskId, verificationId, ...body }) => {
|
|
1595
|
+
try {
|
|
1596
|
+
const data = await apiPatch(config, `/v1/tasks/${taskId}/verifications`, { projectId: resolveProjectId(projectId), id: verificationId, ...body })
|
|
1597
|
+
return textResult(data)
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
return errorResult(err)
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
server.registerTool(
|
|
1605
|
+
'delete_verification',
|
|
1606
|
+
{
|
|
1607
|
+
description: 'Borra una verificación de una tarea (ej. se registró por error o en la plataforma equivocada). Si la verificación sigue aplicando pero no se va a hacer, no la borres: márcala "waived" con update_verification y una nota, para que quede rastro.',
|
|
1608
|
+
inputSchema: {
|
|
1609
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1610
|
+
taskId: z.string().describe('ID de la tarea'),
|
|
1611
|
+
verificationId: z.string().describe('ID de la verificación a borrar'),
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
async ({ projectId, taskId, verificationId }) => {
|
|
1615
|
+
try {
|
|
1616
|
+
const data = await apiDelete(config, `/v1/tasks/${taskId}/verifications`, { projectId: resolveProjectId(projectId), id: verificationId })
|
|
1617
|
+
return textResult(data)
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
return errorResult(err)
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
server.registerTool(
|
|
1625
|
+
'verify_with_agent',
|
|
1626
|
+
{
|
|
1627
|
+
description: 'Encola una corrida de agente que confirma una verificación siguiendo sus instrucciones. Solo para verificaciones con mode "agent" o "either", y requiere un agente con revisión con IA activada (aiReviewEnabled). Sin agentId, el server elige el agente por la plataforma (web→"web", ios→"ios", android→"android", api→"node"). La corrida queda "queued": al terminar marca la verificación aprobada/fallida sola (inconclusa la deja pending con un comentario) -- nunca la des por aprobada sin confirmar con get_agent_run.',
|
|
1628
|
+
inputSchema: {
|
|
1629
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1630
|
+
taskId: z.string().describe('ID de la tarea'),
|
|
1631
|
+
verificationId: z.string().describe('ID de la verificación a confirmar'),
|
|
1632
|
+
agentId: z.string().optional().describe('Agente específico (ver list_agents) -- si no se manda, se resuelve por la plataforma de la verificación'),
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
async ({ projectId, taskId, verificationId, ...body }) => {
|
|
1636
|
+
try {
|
|
1637
|
+
const data = await apiPost(config, `/v1/tasks/${taskId}/verifications/${verificationId}/agent-run`, { projectId: resolveProjectId(projectId), ...body })
|
|
1638
|
+
return textResult(data)
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
return errorResult(err)
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1407
1645
|
// Adjuntos (imágenes + código) en tasks/notes/ideas/docs -- mismo criterio
|
|
1408
1646
|
// que checklist: entityType/entityId van en el path (dos segmentos
|
|
1409
1647
|
// dinámicos), así que server.registerTool directo. Ver
|
|
@@ -1642,6 +1880,7 @@ export function createServer() {
|
|
|
1642
1880
|
'/v1/ideas',
|
|
1643
1881
|
'Crea una idea en un proyecto de Orquestra.',
|
|
1644
1882
|
{
|
|
1883
|
+
relatedTo: RELATED_TO,
|
|
1645
1884
|
title: z.string().describe('Título de la idea'),
|
|
1646
1885
|
description: z.string().optional(),
|
|
1647
1886
|
priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium -- se puede derivar de impact+effort si los mandas (alto+bajo→high, alto+alto o bajo+bajo→medium, bajo+alto→low).'),
|
|
@@ -1840,6 +2079,7 @@ export function createServer() {
|
|
|
1840
2079
|
'/v1/changelog',
|
|
1841
2080
|
'Crea una entrada de changelog en un proyecto de Orquestra (estilo Keep a Changelog).',
|
|
1842
2081
|
{
|
|
2082
|
+
relatedTo: RELATED_TO,
|
|
1843
2083
|
section: z.enum(CHANGELOG_SECTIONS).describe('Sección del changelog'),
|
|
1844
2084
|
text: z.string().describe('Descripción de la entrada'),
|
|
1845
2085
|
version: z.string().optional(),
|
|
@@ -1890,6 +2130,7 @@ export function createServer() {
|
|
|
1890
2130
|
'/v1/notes',
|
|
1891
2131
|
'Crea una nota en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea o idea del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
|
|
1892
2132
|
{
|
|
2133
|
+
relatedTo: RELATED_TO,
|
|
1893
2134
|
title: z.string().describe('Título de la nota'),
|
|
1894
2135
|
body: z.string().optional(),
|
|
1895
2136
|
pinned: z.boolean().optional(),
|
|
@@ -1965,6 +2206,7 @@ export function createServer() {
|
|
|
1965
2206
|
'/v1/docs',
|
|
1966
2207
|
'Crea un documento en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea o idea del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
|
|
1967
2208
|
{
|
|
2209
|
+
relatedTo: RELATED_TO,
|
|
1968
2210
|
type: z.enum(DOC_TYPES).optional().describe('Tipo de documento -- obligatorio salvo que mandes templateId (la plantilla trae su tipo)'),
|
|
1969
2211
|
title: z.string().describe('Título del documento'),
|
|
1970
2212
|
content: z.string().optional(),
|
|
@@ -2085,6 +2327,7 @@ export function createServer() {
|
|
|
2085
2327
|
}
|
|
2086
2328
|
|
|
2087
2329
|
registerCreate('create_service', '/v1/services', 'Crea un servicio compartido de infraestructura (backend, base de datos, auth, etc.) en un proyecto. Además de status/notes, acepta los campos específicos de cada tipo (ej. apiUrl/region para backend, engine/provider para database) -- manda solo los que apliquen a `type`.', {
|
|
2330
|
+
relatedTo: RELATED_TO,
|
|
2088
2331
|
type: z.enum(SERVICE_TYPES).describe('Tipo de servicio'),
|
|
2089
2332
|
name: z.string().describe('Nombre del servicio'),
|
|
2090
2333
|
status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
|
|
@@ -2150,6 +2393,7 @@ export function createServer() {
|
|
|
2150
2393
|
})
|
|
2151
2394
|
|
|
2152
2395
|
registerCreate('create_distribution', '/v1/distributions', 'Crea una distribución/plataforma (iOS, Android, Web, etc.) en un proyecto.', {
|
|
2396
|
+
relatedTo: RELATED_TO,
|
|
2153
2397
|
type: z.enum(PLATFORM_TYPES).describe('Tipo de plataforma'),
|
|
2154
2398
|
label: z.string().describe('Nombre a mostrar por default'),
|
|
2155
2399
|
name: z.string().optional().describe('Nombre editable -- si no se manda, la UI usa label'),
|
|
@@ -2287,6 +2531,7 @@ export function createServer() {
|
|
|
2287
2531
|
{}, (args, projectId) => ({ projectId }))
|
|
2288
2532
|
|
|
2289
2533
|
registerCreate('create_risk', '/v1/risks', 'Registra un riesgo/bloqueo a nivel de proyecto -- algo que puede hacer fallar o atrasar el proyecto en general, no el bloqueo puntual de una tarea (para eso usa relatedTaskId en update_task).', {
|
|
2534
|
+
relatedTo: RELATED_TO,
|
|
2290
2535
|
title: z.string(),
|
|
2291
2536
|
description: z.string().optional(),
|
|
2292
2537
|
probability: z.enum(RISK_LEVELS).optional().describe('Default: medium'),
|
|
@@ -2313,6 +2558,7 @@ export function createServer() {
|
|
|
2313
2558
|
{}, (args, projectId) => ({ projectId }))
|
|
2314
2559
|
|
|
2315
2560
|
registerCreate('create_content', '/v1/content', 'Crea una pieza de contenido. Dos formas: (1) channelId suelto -- 1 canal, shape simple, seguí usándolo si solo publicás en un lado. (2) channels[] -- una pieza multi-canal con una variante por canal (cada una con su propio caption/status/fecha/desempeño), reemplaza tener que llamar esto varias veces para la "misma" publicación en distintos canales.', {
|
|
2561
|
+
relatedTo: RELATED_TO,
|
|
2316
2562
|
channelId: z.string().optional().describe('Canal donde se publica -- ver list_channels. Usalo para una pieza de 1 solo canal; si vas a publicar en varios, mandá channels[] en vez de esto.'),
|
|
2317
2563
|
channels: z.array(CONTENT_VARIANT_SCHEMA).optional().describe('Pieza multi-canal -- una entrada por canal. No mandes channelId si usás esto.'),
|
|
2318
2564
|
title: z.string().describe('Referencia interna, no es lo que se publica -- compartida entre todos los canales si es multi-canal'),
|
|
@@ -2442,6 +2688,7 @@ export function createServer() {
|
|
|
2442
2688
|
runnerId: z.string().optional().describe('Máquina específica -- si se manda, gana sobre la del agente resuelto'),
|
|
2443
2689
|
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.'),
|
|
2444
2690
|
moduleId: z.string().optional().describe('Igual que taskId pero acota a un módulo completo. No combinar con taskId.'),
|
|
2691
|
+
verificationId: z.string().optional().describe('Verificación de la tarea (requiere taskId) que esta corrida debe confirmar siguiendo sus instrucciones -- al terminar, el resultado la marca aprobada/fallida solo. Solo para verificaciones con mode "agent" o "either" y un agente con revisión con IA activada. Preferí verify_with_agent, que resuelve el agente por la plataforma.'),
|
|
2445
2692
|
})
|
|
2446
2693
|
|
|
2447
2694
|
server.registerTool(
|