orquestra-mcp 1.31.0 → 1.33.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.mjs +124 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.31.0",
3
+ "version": "1.33.0",
4
4
  "description": "Servidor MCP de Orquestra -- 115 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": {
package/src/server.mjs CHANGED
@@ -26,6 +26,10 @@ function textResult(data) {
26
26
  const PROJECT_ID_DESC = 'ID del proyecto dentro del workspace configurado. Si hay un proyecto default configurado (via "setup" o ORQUESTRA_DEFAULT_PROJECT_ID), este argumento se ignora -- el server queda fijo a ese proyecto y no puede leer/escribir otro.'
27
27
 
28
28
  const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'design', 'security', 'meeting', 'call', 'followup', 'business']
29
+ // Actividades (ORQ-295): reunión/llamada/seguimiento. 'business' sigue siendo tarea (entregable). Mismas listas que
30
+ // orquestra-infra/functions/api/lib/activities.mjs y schema/enums/ActivityStatus.json.
31
+ const ACTIVITY_TYPES = ['meeting', 'call', 'followup']
32
+ const ACTIVITY_STATUSES = ['scheduled', 'done', 'cancelled']
29
33
  const TASK_PRIORITIES = ['high', 'medium', 'low']
30
34
  const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
31
35
  // 'postmortem' faltaba acá (gap preexistente, encontrado 12 ago 2026) --
@@ -95,13 +99,15 @@ Analítica y anuncios en un sitio web: antes de instalar o tocar Google Analytic
95
99
 
96
100
  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"):
97
101
  - 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
+ - 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.
98
103
  - 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.
99
104
  - 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
+ - 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).
100
106
  - 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.
101
107
  - 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.
102
108
  - 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.
103
109
  - 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.
104
- - Paridad por producto: las distribuciones que son el mismo producto en distintas plataformas (típicamente Web/iOS/Android) comparten un productKey (list_distributions lo muestra). Si al crear una tarea con scope en una de ellas la respuesta trae parityAddedDistIds, el server sumó las hermanas como pendientes -- dícelo al usuario ("también quedó pendiente en iOS y Android") y no lo deshagas salvo que el trabajo sea genuinamente de una sola plataforma (entonces skipParity:true, o quítalas con update_task/scope explicando por qué). Al cerrar una tarea con scope multiplataforma, marca done:true SOLO en la plataforma donde de verdad se hizo (update_task/scope) -- el estatus "Listo" no mueve esos flags solo; dejar las hermanas en done:false es exactamente lo que hace que el desfase aparezca en get_parity_summary y en la revisión de sincronización (tasksClosedWithParityPending), que es el punto: que nunca se olvide el trabajo del otro lado. Si el proyecto tiene 2+ plataformas y ninguna tiene productKey (get_parity_summary → productGroups.configured=false), pregúntale al usuario cuáles son el mismo producto y configúralo con update_distribution -- nunca lo infieras solo del tipo de plataforma.
110
+ - Paridad por producto: las distribuciones que son el mismo producto en distintas plataformas (típicamente Web/iOS/Android) comparten un productKey (list_distributions lo muestra). Si al crear una tarea con scope en una de ellas la respuesta trae parityAddedDistIds, el server sumó las hermanas como pendientes -- dícelo al usuario ("también quedó pendiente en iOS y Android") y no lo deshagas salvo que el trabajo sea genuinamente de una sola plataforma (entonces skipParity:true, o quítalas con update_task/scope explicando por qué). Al cerrar una tarea con scope multiplataforma, marca done:true SOLO en la plataforma donde de verdad se hizo (update_task/scope) -- el estatus "Listo" no mueve esos flags solo; cuando un commit o PR del repo de otra plataforma menciona el código de la tarea (ej. "(ORQ-123)"), el servidor propone solo una sugerencia scope_done_proposal (list_suggestions) para marcar esa plataforma como hecha: si la ves, confirma con el código real y acéptala con accept_suggestion en vez de editar el scope a mano. Si el trabajo de otra plataforma ya vive en OTRA tarea (una tarea gemela, ej. "[iOS] ..." y "[Android] ..."), no dupliques scope: liga la hermana con update_task parityOf=<id de la tarea origen> y en paridad cuentan como un solo item; al cerrar la hermana su plataforma queda hecha sola. Dejar las hermanas en done:false es exactamente lo que hace que el desfase aparezca en get_parity_summary y en la revisión de sincronización (tasksClosedWithParityPending), que es el punto: que nunca se olvide el trabajo del otro lado. Si el proyecto tiene 2+ plataformas y ninguna tiene productKey (get_parity_summary → productGroups.configured=false), pregúntale al usuario cuáles son el mismo producto y configúralo con update_distribution -- nunca lo infieras solo del tipo de plataforma.
105
111
  - 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.
106
112
  - 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í.
107
113
  - 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.
@@ -123,6 +129,15 @@ Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene cód
123
129
 
124
130
  Si el usuario pregunta explícito qué Claude Skills de terceros le faltan (o pide instalar alguna) -- no lo hagas sin que te lo pidan: llamá a get_skills_catalog y revisá con acceso real al filesystem cuáles de esos keys ya existen en .claude/skills/ (del proyecto) o ~/.claude/skills/ (global). Antes de instalar cualquiera con install_skill, llamá primero a preview_skill y mostrale al usuario el skillMdContent completo -- nunca instales sin que haya visto ese contenido y confirmado explícitamente, los SKILL.md de terceros pueden traer instrucciones que un agente ejecuta solo con cargarlas.`
125
131
 
132
+ // Evidencia opcional por plataforma (Paridad v2 fase 1). doneAt/doneBy/doneSource los estampa el server al marcar done:true;
133
+ // mandarlos solo hace falta para registrar algo que ocurrió antes (ej. un commit viejo).
134
+ const SCOPE_EVIDENCE = {
135
+ doneAt: z.number().optional().describe('ms desde epoch de cuando se hizo en esa plataforma; el server lo estampa solo al pasar done a true'),
136
+ doneSource: z.enum(['manual', 'github', 'sibling']).optional(),
137
+ commitSha: z.string().optional().describe('commit que respalda que ya está hecho en esa plataforma'),
138
+ commitUrl: z.string().optional(),
139
+ }
140
+
126
141
  export function createServer() {
127
142
  const config = loadConfig()
128
143
  const server = new McpServer({ name: 'orquestra-mcp', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS })
@@ -205,7 +220,7 @@ export function createServer() {
205
220
  registerRead(
206
221
  'list_tasks',
207
222
  '/v1/tasks',
208
- 'Lista las tareas de un proyecto de Orquestra, opcionalmente filtradas por estatus/módulo/tipo/prioridad/responsable/tag. Los filtros se combinan con AND -- pásalos en vez de traer todo el proyecto y filtrar tú mismo del lado del cliente. Por default excluye tareas cerradas (isFinal/isCancel/isArchive) -- un proyecto con historial acumula cientos, traerlas todas revienta el contexto sin que nadie las haya pedido. Si mandas statusId puntual, eso ya es una elección explícita y no se le suma este filtro (podés pedir "solo Listo"). Para historial/auditoría real, usa includeClosed=true.',
223
+ 'Lista las tareas de un proyecto de Orquestra, opcionalmente filtradas por estatus/módulo/tipo/prioridad/responsable/tag. Los filtros se combinan con AND -- pásalos en vez de traer todo el proyecto y filtrar tú mismo del lado del cliente. Por default excluye tareas cerradas (isFinal/isCancel/isArchive) -- un proyecto con historial acumula cientos, traerlas todas revienta el contexto sin que nadie las haya pedido. Si mandas statusId puntual, eso ya es una elección explícita y no se le suma este filtro (podés pedir "solo Listo"). Para historial/auditoría real, usa includeClosed=true. No incluye actividades (reuniones/llamadas/seguimientos) -- esas van con list_activities.',
209
224
  {
210
225
  statusId: z.string().optional().describe('ID de estatus para filtrar (opcional) -- pedir un estatus puntual (incluso uno cerrado) siempre se respeta tal cual, sin el filtro default de abiertas'),
211
226
  moduleId: z.string().optional().describe('ID de módulo para filtrar (opcional)'),
@@ -230,10 +245,10 @@ export function createServer() {
230
245
  registerRead(
231
246
  'search',
232
247
  '/v1/search',
233
- 'Busca por texto en tareas, notas, ideas y docs de un proyecto de Orquestra. Tope de 30 resultados con las tareas primero: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
248
+ 'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra. Tope de 30 resultados con las tareas primero: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
234
249
  {
235
250
  q: z.string().describe('Texto a buscar'),
236
- types: z.array(z.enum(['task', 'note', 'idea', 'doc'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en los cuatro.'),
251
+ types: z.array(z.enum(['task', 'activity', 'note', 'idea', 'doc'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en todos.'),
237
252
  },
238
253
  (args, projectId) => ({ projectId, q: args.q, types: args.types?.join(',') })
239
254
  )
@@ -595,7 +610,7 @@ export function createServer() {
595
610
  registerCreate(
596
611
  'create_task',
597
612
  '/v1/tasks',
598
- '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.',
613
+ '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).',
599
614
  {
600
615
  title: z.string().describe('Título de la tarea'),
601
616
  description: z.string().optional(),
@@ -608,9 +623,11 @@ export function createServer() {
608
623
  reviewDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('Fecha para volver a MIRAR esta tarea -- recordatorio, NO un entregable. Para un aviso CON HORA (ej. en 5 horas) usa set_task_reminder. Úsalo para tareas de espera sin deadline real (ej. "esperar respuesta de Apple y revisar en 5 días"): en ese caso NO llenes dueDate, llena reviewDate. Distinto de dueDate (fecha de término real de un entregable) -- una tarea puede tener ambos, uno solo, o ninguno. Dispara un aviso (notificación + correo) al assignedTo el día que llega, un solo disparo.'),
609
624
  assignedTo: z.string().optional().describe('uid del miembro asignado'),
610
625
  tags: z.array(z.string()).optional(),
611
- scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
626
+ scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean(), ...SCOPE_EVIDENCE })).optional()
612
627
  .describe('Alcance por distribución (paridad multiplataforma) -- para setearlo desde la creación en vez de crear y después update_task. Usa get_parity_summary/list_distributions para ver qué distId existen. Si una distId tiene hermanas (mismo productKey), el server suma las que falten solo, con done:false -- la respuesta trae parityAddedDistIds con lo que agregó: avísale al usuario que la tarea también quedó pendiente en esas plataformas.'),
613
628
  skipParity: z.boolean().optional().describe('true = NO sumar las plataformas hermanas al scope aunque tengan el mismo productKey. Solo cuando el trabajo es genuinamente de UNA plataforma (ej. un ajuste de Gradle que no tiene contraparte en iOS) -- no lo mandes por default. Las tareas type="bug" nunca propagan, sin necesidad de esto.'),
629
+ parityExempt: z.boolean().optional().describe('true = este trabajo es de UNA sola plataforma y no cuenta en get_parity_summary (se pone solo cuando skipParity va con scope de una sola dist). false para volver a contarla.'),
630
+ parityOf: z.string().optional().describe('Folio: id de la tarea ORIGEN de la que esta es la hermana para otra plataforma (ej. la versión iOS de una tarea de Android). En paridad cuentan como UN item y al cerrar esta, su plataforma queda hecha en el origen. El origen no puede ser hermana de otra. "" lo quita.'),
614
631
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea. LEGACY desde ORQ-248: si vas a bloquear por más de una tarea, usa blockedByIds en vez de esto (relatedTaskId se sigue llenando solo, como blockedByIds[0], para paridad con iOS/Android).'),
615
632
  blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- ids de otras tareas (puede ser más de una). Esta tarea se muestra como bloqueada mientras ALGUNA de esas no llegue a un estatus final (isFinal). Reemplaza a relatedTaskId para bloqueos múltiples -- preferí este campo, no relatedTaskId, salvo que de verdad sea un solo bloqueador.'),
616
633
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call" (plataforma o lugar, ej. "Zoom", "Oficina") o "followup"/"business" (contexto libre) -- ignóralo para el resto de los tipos.'),
@@ -645,8 +662,10 @@ export function createServer() {
645
662
  reviewDate: z.string().optional().describe('YYYY-MM-DD. Ver create_task -- recordatorio para volver a revisar, distinto de dueDate. Mandá "" para quitarlo.'),
646
663
  assignedTo: z.string().optional(),
647
664
  tags: z.array(z.string()).optional(),
648
- scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
665
+ scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean(), ...SCOPE_EVIDENCE })).optional()
649
666
  .describe('Alcance por distribución -- usa get_parity_summary para ver el scope actual antes de editarlo'),
667
+ parityExempt: z.boolean().optional().describe('true = este trabajo es de UNA sola plataforma y no cuenta en get_parity_summary; false para volver a contarla.'),
668
+ parityOf: z.string().optional().describe('Folio: id de la tarea ORIGEN de la que esta es la hermana para otra plataforma (ej. la versión iOS de una tarea de Android). En paridad cuentan como UN item y al cerrar esta, su plataforma queda hecha en el origen. El origen no puede ser hermana de otra. "" lo quita.'),
650
669
  relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. LEGACY, ver create_task -- preferí blockedByIds.'),
651
670
  blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- reemplaza el array COMPLETO (no hace merge), [] quita todos los bloqueos. Ver create_task.'),
652
671
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call"/"followup"/"business". Ver create_task.'),
@@ -666,6 +685,103 @@ export function createServer() {
666
685
  }
667
686
  )
668
687
 
688
+ // ═══ ACTIVIDADES (ORQ-295) ═══════════════════════════════════════════════════
689
+ // Reunión/llamada/seguimiento: misma colección que las tareas en la
690
+ // API (POST/PATCH /v1/tasks con type de actividad), pero sin módulo
691
+ // obligatorio, fuera del kanban y con su propio ciclo (activityStatus). La
692
+ // hora se pasa a scheduledAt ACÁ, en la máquina del usuario, porque la API
693
+ // no conoce su zona horaria (mismo motivo que set_task_reminder).
694
+ const toScheduledAt = (date, time) => {
695
+ if (!time) return undefined
696
+ const ms = new Date(`${date}T${time}:00`).getTime()
697
+ if (Number.isNaN(ms)) throw new Error(`Fecha/hora inválida: ${date} ${time}`)
698
+ return ms
699
+ }
700
+ const ACTIVITY_FIELDS_SCHEMA = {
701
+ location: z.string().optional().describe('Plataforma o lugar ("Zoom", "Oficina del cliente") o contexto libre para un seguimiento.'),
702
+ participants: z.array(z.string()).optional().describe('Nombres libres de quienes participan (cliente, equipo) -- no uids.'),
703
+ description: z.string().optional().describe('Agenda o notas previas.'),
704
+ outcome: z.string().optional().describe('Resultado / minuta corta -- qué se acordó. Llénalo al marcarla hecha.'),
705
+ nextStep: z.string().optional().describe('Siguiente paso acordado. Si es trabajo real, además crea la tarea con create_task y liga su id en nextStepTaskId.'),
706
+ nextStepTaskId: z.string().optional().describe('id de la tarea creada desde nextStep.'),
707
+ moduleId: z.string().optional().describe('Opcional -- a diferencia de las tareas, una actividad no necesita módulo.'),
708
+ assignedTo: z.string().optional().describe('uid del responsable'),
709
+ recurrence: z.object({ freq: z.enum(['daily', 'weekly', 'monthly']) }).nullable().optional()
710
+ .describe('Repetición (ej. junta semanal): al marcarla hecha se agenda sola la siguiente, misma hora y participantes. null la quita.'),
711
+ }
712
+
713
+ server.registerTool(
714
+ 'log_activity',
715
+ {
716
+ description: 'Agenda o registra una actividad -- reunión, llamada o seguimiento -- en un proyecto de Orquestra. No es una tarea: no lleva módulo obligatorio ni pasa por el kanban; aparece en la Agenda y en el calendario. Para una que YA pasó, manda activityStatus "done" con outcome (y nextStep si hubo). Si el siguiente paso es trabajo real, crea además la tarea con create_task.',
717
+ inputSchema: {
718
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
719
+ type: z.enum(ACTIVITY_TYPES).describe('meeting = reunión, call = llamada, followup = seguimiento'),
720
+ title: z.string().describe('Título de la actividad'),
721
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').describe('Día de la actividad (YYYY-MM-DD, hora local del usuario).'),
722
+ time: z.string().regex(/^\d{2}:\d{2}$/, 'Formato esperado: HH:MM').optional().describe('Hora local (HH:MM, 24 h), si se sabe.'),
723
+ activityStatus: z.enum(ACTIVITY_STATUSES).optional().describe('Default "scheduled" (programada). "done" para registrar una que ya pasó.'),
724
+ ...ACTIVITY_FIELDS_SCHEMA,
725
+ },
726
+ },
727
+ async ({ projectId, date, time, ...body }) => {
728
+ try {
729
+ const scheduledAt = toScheduledAt(date, time)
730
+ const data = await apiPost(config, '/v1/tasks', {
731
+ projectId: resolveProjectId(projectId), ...body, dueDate: date, ...(scheduledAt ? { scheduledAt } : {}),
732
+ })
733
+ return textResult(data)
734
+ } catch (err) {
735
+ return errorResult(err)
736
+ }
737
+ }
738
+ )
739
+
740
+ server.registerTool(
741
+ 'update_activity',
742
+ {
743
+ description: 'Edita una actividad (reunión/llamada/seguimiento) de Orquestra: reprogramarla, marcarla hecha con su resultado (activityStatus "done" + outcome + nextStep) o cancelarla. El server mueve solo el estatus interno; no hay que validar transiciones del kanban.',
744
+ inputSchema: {
745
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
746
+ id: z.string().describe('ID de la actividad (ver list_activities)'),
747
+ title: z.string().optional(),
748
+ type: z.enum(ACTIVITY_TYPES).optional(),
749
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('Nuevo día (YYYY-MM-DD). Si cambias el día y había hora, manda también time.'),
750
+ time: z.string().regex(/^\d{2}:\d{2}$/, 'Formato esperado: HH:MM').optional().describe('Nueva hora local (HH:MM). Requiere date.'),
751
+ activityStatus: z.enum(ACTIVITY_STATUSES).optional(),
752
+ ...ACTIVITY_FIELDS_SCHEMA,
753
+ },
754
+ },
755
+ async ({ projectId, id, date, time, ...body }) => {
756
+ try {
757
+ if (time && !date) throw new Error('Para cambiar la hora manda también date (YYYY-MM-DD).')
758
+ const scheduledAt = date ? toScheduledAt(date, time) : undefined
759
+ const data = await apiPatch(config, '/v1/tasks', {
760
+ projectId: resolveProjectId(projectId), id, ...body,
761
+ ...(date ? { dueDate: date } : {}), ...(scheduledAt ? { scheduledAt } : {}),
762
+ })
763
+ return textResult(data)
764
+ } catch (err) {
765
+ return errorResult(err)
766
+ }
767
+ }
768
+ )
769
+
770
+ registerRead(
771
+ 'list_activities',
772
+ '/v1/activities',
773
+ 'Lista las actividades (reuniones, llamadas, seguimientos) de un proyecto de Orquestra. status "scheduled" = agenda (próximas primero), "done" = bitácora (más recientes primero); sin status trae todas. from/to acotan por día.',
774
+ {
775
+ status: z.enum(ACTIVITY_STATUSES).optional(),
776
+ type: z.enum(ACTIVITY_TYPES).optional(),
777
+ moduleId: z.string().optional(),
778
+ from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Desde (YYYY-MM-DD, inclusive)'),
779
+ to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Hasta (YYYY-MM-DD, inclusive)'),
780
+ limit: z.number().optional().describe('Default 100'),
781
+ },
782
+ (args, projectId) => ({ projectId, status: args.status, type: args.type, moduleId: args.moduleId, from: args.from, to: args.to, limit: args.limit })
783
+ )
784
+
669
785
  // Fusionar tareas (20 ago 2026) -- registradas directo con
670
786
  // server.registerTool (no registerCreate/registerUpdate) porque son
671
787
  // POST a `${path}/${id}/merge|unmerge`, no un CRUD estándar. Fusión
@@ -1763,7 +1879,7 @@ export function createServer() {
1763
1879
 
1764
1880
  // ═══ ESCRITURA (nivel B -- módulos, flujo, categorías, infra) ═══════════════
1765
1881
 
1766
- const SCOPE_SCHEMA = z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean().optional() })).optional()
1882
+ const SCOPE_SCHEMA = z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean().optional(), ...SCOPE_EVIDENCE })).optional()
1767
1883
 
1768
1884
  registerCreate('create_module', '/v1/modules', 'Crea un módulo en un proyecto de Orquestra.', {
1769
1885
  name: z.string().describe('Nombre del módulo'),