orquestra-mcp 1.52.1 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # orquestra-mcp
2
2
 
3
- Servidor MCP (Model Context Protocol) para Orquestra. Expone **144 tools**
3
+ Servidor MCP (Model Context Protocol) para Orquestra. Expone **149 tools**
4
4
  (conteo al 24 oct 2026, v1.49.0; la lista real está en `src/server.mjs`)
5
5
  que llaman a la API de `orquestra-infra`: lectura de contexto/dashboard/
6
6
  workspace/infra/paridad/listados, escritura de tareas/ideas/changelog/
@@ -229,7 +229,7 @@ create/update sigue siendo criterio del agente y del humano.
229
229
 
230
230
  ## Estado
231
231
 
232
- 144 tools (ver arriba): lectura (contexto, dashboard, workspace, infra, paridad, standup,
232
+ 149 tools (ver arriba): lectura (contexto, dashboard, workspace, infra, paridad, standup,
233
233
  sync report, setup guide, playbooks de etapa y de plataforma, catálogo de
234
234
  skills, search) + `list_*` de cada colección + creación/edición de tareas
235
235
  (con checklist, verificaciones por plataforma, tiempo, adjuntos, merge), ideas (con comentarios), notas,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.52.1",
3
+ "version": "1.55.0",
4
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": {
package/src/server.mjs CHANGED
@@ -35,6 +35,11 @@ const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'de
35
35
  const ACTIVITY_TYPES = ['meeting', 'call', 'followup']
36
36
  const ACTIVITY_STATUSES = ['scheduled', 'done', 'cancelled']
37
37
  const TASK_PRIORITIES = ['high', 'medium', 'low']
38
+ const WORK_CATEGORIES = ['plan', 'build', 'delivery', 'review']
39
+ const PHASE_STATUSES = ['pending', 'active', 'done']
40
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
41
+ const START_DATE_DESC = 'YYYY-MM-DD, fecha de inicio planeada (opcional). Debe ser <= dueDate (400 si no). "" la quita.'
42
+ const WORK_CATEGORY_DESC = 'Categoría de trabajo: plan | build | delivery | review. Si no la mandas, la API la deriva del type de la tarea; mandarla fija un override. "" quita el override y vuelve a la derivada.'
38
43
  // Relaciones (ORQ-326, ORQ-347) -- misma lista que orquestra-infra/schema/enums/RelationItemType.json.
39
44
  const RELATION_TYPES = ['task', 'activity', 'note', 'doc', 'idea', 'risk', 'content', 'changelog', 'distribution', 'service']
40
45
  // `relatedTo` en los create_* (ORQ-347): de dónde sale lo que se crea. El server valida el destino
@@ -134,7 +139,7 @@ Durante el trabajo normal de código, sin que el usuario te lo pida explícitame
134
139
  - 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.
135
140
  - 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.
136
141
  - 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í.
137
- - Una tarea = UN entregable; un ítem del checklist = UN paso corto de ese entregable. Si el título junta varias cosas con comas o "y" ("X, Y y Z"), o un ítem del checklist tendría su propio deploy, commits, pruebas o alertas (es una integración/función entera, no un paso), son tareas separadas: créalas aparte en el mismo módulo (si son muchas del mismo tema, ese tema es un módulo, no una tarea con 14 ítems). Lo que depende de algo externo o futuro (un contrato, un proveedor, una versión que todavía no sale, la respuesta de una tienda) va en su propia tarea -- en Bloqueado o con reviewDate --, nunca dentro de una tarea que ya está lista, porque la deja abierta indefinidamente. El texto de un ítem es la acción ("Configurar cron diario de la CSL"), no la bitácora: commits, "probado en vivo", tiempos y tamaños van en un comment de la tarea, en resolvedCommitSha o en el changelog. Algo que ya no aplica no se deja como ítem pendiente con "ya no hace falta" en el texto: díselo al usuario y bórralo con delete_checklist_item (con un comment en la tarea explicando por qué). No reorganices tareas que ya existen así salvo que el usuario lo pida -- la regla es para lo nuevo.
142
+ - Una tarea = UN entregable; un ítem del checklist = UN paso corto de ese entregable. Si el título junta varias cosas con comas o "y" ("X, Y y Z"), o un ítem del checklist tendría su propio deploy, commits, pruebas o alertas (es una integración/función entera, no un paso), son tareas separadas: créalas aparte en el mismo módulo (si son muchas del mismo tema, ese tema es un módulo, no una tarea con 14 ítems). Lo que depende de algo externo o futuro (un contrato, un proveedor, una versión que todavía no sale, la respuesta de una tienda) va en su propia tarea -- en Bloqueado o con reviewDate --, nunca dentro de una tarea que ya está lista, porque la deja abierta indefinidamente. El texto de un ítem es la acción ("Configurar cron diario de la CSL"), no la bitácora: commits, "probado en vivo", tiempos y tamaños van en un comment de la tarea, en resolvedCommitSha o en el changelog. Algo que ya no aplica no se deja como ítem pendiente con "ya no hace falta" en el texto ni se marca done: díselo al usuario y descártalo con update_checklist_item skipped:true + skipReason (queda tachado con su motivo, no bloquea el cierre y no cuenta como hecho); delete_checklist_item solo si el ítem fue un error. No reorganices tareas que ya existen así salvo que el usuario lo pida -- la regla es para lo nuevo.
138
143
  - 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.
139
144
  - 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.
140
145
 
@@ -287,6 +292,7 @@ export function createServer() {
287
292
  priority: z.enum(TASK_PRIORITIES).optional().describe('Prioridad para filtrar (opcional)'),
288
293
  assignedTo: z.string().optional().describe('uid del responsable para filtrar (opcional)'),
289
294
  tag: z.string().optional().describe('Un tag exacto para filtrar -- solo uno, no una lista (opcional)'),
295
+ workCategory: z.enum(WORK_CATEGORIES).optional().describe('Categoría de trabajo para filtrar (opcional): plan | build | delivery | review'),
290
296
  includeClosed: z.boolean().optional().describe('Default false -- sin esto, tareas en un estatus isFinal/isCancel/isArchive quedan afuera. Ponelo en true solo si de verdad necesitás el historial cerrado (auditoría, "¿cuándo se hizo X?").'),
291
297
  },
292
298
  (args, projectId) => ({
@@ -297,6 +303,7 @@ export function createServer() {
297
303
  priority: args.priority,
298
304
  assignedTo: args.assignedTo,
299
305
  tag: args.tag,
306
+ workCategory: args.workCategory,
300
307
  includeClosed: args.includeClosed,
301
308
  })
302
309
  )
@@ -771,6 +778,8 @@ export function createServer() {
771
778
  categoryId: z.string().optional(),
772
779
  moduleId: z.string().describe('OBLIGATORIO -- módulo al que pertenece esta tarea, para poder agruparla/revisarla después. Si no existe uno relacionado, llamá primero a list_modules (para no duplicar) y create_module.'),
773
780
  dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').describe('OBLIGATORIO -- fecha estimada de término (YYYY-MM-DD), calculada con criterio real según prioridad/tamaño del trabajo (mismo criterio que la "revisión de sincronización" de get_sync_report), nunca un valor arbitrario.'),
781
+ startDate: z.string().regex(DATE_RE, 'Formato esperado: YYYY-MM-DD').optional().describe(`Inicio planeado de la tarea. ${START_DATE_DESC}`),
782
+ workCategory: z.enum(WORK_CATEGORIES).optional().describe(WORK_CATEGORY_DESC),
774
783
  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.'),
775
784
  assignedTo: z.string().optional().describe('uid del miembro asignado'),
776
785
  tags: z.array(z.string()).optional(),
@@ -806,7 +815,7 @@ export function createServer() {
806
815
  registerUpdate(
807
816
  'update_task',
808
817
  '/v1/tasks',
809
- '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.',
818
+ '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 y descarta con skipped:true + skipReason los que ya no aplican (no bloquean); usa completeChecklist:true solo si el usuario confirma que lo pendiente 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.',
810
819
  {
811
820
  title: z.string().optional(),
812
821
  description: z.string().optional(),
@@ -818,6 +827,8 @@ export function createServer() {
818
827
  categoryId: z.string().optional(),
819
828
  moduleId: z.string().optional(),
820
829
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
830
+ startDate: z.string().optional().describe(`Inicio planeado de la tarea. ${START_DATE_DESC}`),
831
+ workCategory: z.union([z.enum(WORK_CATEGORIES), z.literal('')]).optional().describe(WORK_CATEGORY_DESC),
821
832
  reviewDate: z.string().optional().describe('YYYY-MM-DD. Ver create_task -- recordatorio para volver a revisar, distinto de dueDate. Mandá "" para quitarlo.'),
822
833
  assignedTo: z.string().optional(),
823
834
  tags: z.array(z.string()).optional(),
@@ -861,8 +872,8 @@ export function createServer() {
861
872
  location: z.string().optional().describe('Plataforma o lugar ("Zoom", "Oficina del cliente") o contexto libre para un seguimiento.'),
862
873
  participants: z.array(z.string()).optional().describe('Nombres libres de quienes participan (cliente, equipo) -- no uids.'),
863
874
  description: z.string().optional().describe('Notas previas.'),
864
- agendaItems: z.array(z.union([z.string(), z.object({ id: z.string().optional(), text: z.string() })])).optional()
865
- .describe('Puntos a revisar en la reunión (ej. "Revisar [[ORQ-123]]"). Lista simple, NO checklist. Reemplaza la lista COMPLETA: para agregar uno, lee la actual con list_activities y manda todos (con sus id para conservarlos).'),
875
+ agendaItems: z.array(z.union([z.string(), z.object({ id: z.string().optional(), text: z.string(), group: z.string().max(60).optional(), createdAt: z.number().optional(), createdBy: z.string().optional() })])).optional()
876
+ .describe('Puntos a revisar en la reunión (ej. "Revisar [[ORQ-123]]"). Lista simple, NO checklist. Cada punto puede llevar group (agrupa puntos bajo un encabezado, máx. 60 caracteres). Reemplaza la lista COMPLETA: para agregar uno, lee la actual con list_activities y manda todos, con sus id para conservarlos Y con su group -- si reenvías un punto sin group, pierde su grupo.'),
866
877
  outcome: z.string().optional().describe('Resultado / minuta corta -- qué se acordó. Llénalo al marcarla hecha.'),
867
878
  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.'),
868
879
  nextStepTaskId: z.string().optional().describe('id de la tarea creada desde nextStep.'),
@@ -1570,7 +1581,7 @@ export function createServer() {
1570
1581
  server.registerTool(
1571
1582
  'list_checklist_items',
1572
1583
  {
1573
- description: 'Lista los ítems del checklist de una tarea de Orquestra (texto, hecho/pendiente, responsable).',
1584
+ description: 'Lista los ítems del checklist de una tarea de Orquestra (texto, hecho/pendiente, responsable, y si está descartado como «No aplica»: skipped + skipReason). Un ítem descartado no bloquea el cierre ni cuenta como hecho.',
1574
1585
  inputSchema: {
1575
1586
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1576
1587
  taskId: z.string().describe('ID de la tarea'),
@@ -1610,7 +1621,7 @@ export function createServer() {
1610
1621
  server.registerTool(
1611
1622
  'update_checklist_item',
1612
1623
  {
1613
- description: 'Edita un ítem del checklist de una tarea de Orquestra: texto, hecho/pendiente, o responsable.',
1624
+ description: 'Edita un ítem del checklist de una tarea de Orquestra: texto, hecho/pendiente, responsable, o descartarlo como «No aplica». Para un paso que ya no hace falta usa skipped:true + skipReason (queda tachado con su motivo, no bloquea el cierre y no cuenta como hecho) en vez de marcarlo done o borrarlo; skipped:false lo devuelve a pendiente. Cada descarte queda comentado en la tarea.',
1614
1625
  inputSchema: {
1615
1626
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1616
1627
  taskId: z.string().describe('ID de la tarea'),
@@ -1618,6 +1629,8 @@ export function createServer() {
1618
1629
  text: z.string().optional(),
1619
1630
  done: z.boolean().optional(),
1620
1631
  assignedTo: z.string().optional().describe('uid del miembro responsable, o "" para quitarlo'),
1632
+ skipped: z.boolean().optional().describe('true = «No aplica» (exige skipReason; fuerza done:false); false = volver a pendiente'),
1633
+ skipReason: z.string().optional().describe('Motivo del descarte -- obligatorio con skipped:true, concreto ("se resolvió con X", "el proveedor ya lo hace")'),
1621
1634
  },
1622
1635
  },
1623
1636
  async ({ projectId, taskId, itemId, ...body }) => {
@@ -1641,7 +1654,7 @@ export function createServer() {
1641
1654
  server.registerTool(
1642
1655
  'delete_checklist_item',
1643
1656
  {
1644
- description: 'Borra un ítem del checklist de una tarea de Orquestra. Único borrado real que expone este MCP -- usalo solo cuando de verdad no tiene sentido dejar rastro (ej. como parte de convert_checklist_item_to_task); para "ya no aplica" en general, preferí marcarlo done en vez de borrarlo si querés conservar el historial.',
1657
+ description: 'Borra un ítem del checklist de una tarea de Orquestra. Único borrado real que expone este MCP -- usalo solo cuando el ítem fue un error y no tiene sentido dejar rastro (ej. como parte de convert_checklist_item_to_task). Para un paso que ya no aplica NO lo borres ni lo marques done: usa update_checklist_item con skipped:true + skipReason, que conserva el historial.',
1645
1658
  inputSchema: {
1646
1659
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1647
1660
  taskId: z.string().describe('ID de la tarea'),
@@ -2395,6 +2408,7 @@ export function createServer() {
2395
2408
  scope: SCOPE_SCHEMA.describe('Alcance por distribución, ej: [{distId, layer, done}]'),
2396
2409
  tags: z.array(z.string()).optional(),
2397
2410
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
2411
+ startDate: z.string().optional().describe(`Inicio planeado del módulo. ${START_DATE_DESC}`),
2398
2412
  shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
2399
2413
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2400
2414
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
@@ -2409,6 +2423,7 @@ export function createServer() {
2409
2423
  scope: SCOPE_SCHEMA,
2410
2424
  tags: z.array(z.string()).optional(),
2411
2425
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
2426
+ startDate: z.string().optional().describe(`Inicio planeado del módulo. ${START_DATE_DESC}`),
2412
2427
  shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
2413
2428
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2414
2429
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
@@ -2416,6 +2431,68 @@ export function createServer() {
2416
2431
  key: MODULE_KEY.describe(`${MODULE_KEY.description} Poner o cambiar la clave numera las tareas del módulo que todavía no tienen una (por orden de creación; la respuesta trae assignedCodes). "" la quita: las tareas conservan la suya.`),
2417
2432
  })
2418
2433
 
2434
+ // ═══ FASES DEL PLAN (ORQ-438) ═══════════════════════════════════════════════
2435
+ // Fases con fechas relativas al startDate del proyecto (offsetDays/durationDays);
2436
+ // la API calcula effectiveStartDate/effectiveEndDate. milestones y
2437
+ // acceptanceCriteria se REEMPLAZAN completos en cada PATCH.
2438
+ const PHASE_MILESTONES = z.array(z.object({
2439
+ id: z.string().optional(),
2440
+ title: z.string(),
2441
+ offsetDays: z.number().int().min(0).optional().describe('Días desde el inicio de la fase'),
2442
+ fixedDate: z.string().regex(DATE_RE, 'Formato esperado: YYYY-MM-DD').optional().describe('Fecha fija (sustituye al offsetDays)'),
2443
+ done: z.boolean().optional(),
2444
+ })).optional().describe('Hitos de la fase. REEMPLAZA la lista completa: para marcar uno como hecho, lee la actual con list_phases y reenvía todos (con sus id) cambiando solo done.')
2445
+ const PHASE_CRITERIA = z.array(z.union([z.string(), z.object({ id: z.string().optional(), text: z.string(), done: z.boolean().optional() })])).optional()
2446
+ .describe('Criterios de aceptación de la fase (strings sueltos o {id,text,done}). REEMPLAZA la lista completa: para marcar uno como hecho, lee la actual con list_phases y reenvía todos (con sus id) cambiando solo done.')
2447
+ const PHASE_FIELDS = {
2448
+ name: z.string().describe('Nombre de la fase'),
2449
+ description: z.string().optional(),
2450
+ order: z.number().optional().describe('Posición de la fase en el plan'),
2451
+ offsetDays: z.number().int().min(0).optional().describe('Días desde el startDate del proyecto hasta que arranca la fase (>= 0)'),
2452
+ durationDays: z.number().int().min(1).optional().describe('Duración en días (>= 1)'),
2453
+ status: z.enum(PHASE_STATUSES).optional().describe('pending | active | done'),
2454
+ fixedStartDate: z.string().regex(DATE_RE, 'Formato esperado: YYYY-MM-DD').nullable().optional()
2455
+ .describe('Mueve la fase a una fecha de inicio fija (YYYY-MM-DD). EXIGE fixedStartReason (el motivo del cambio de fecha). null la quita y la fase vuelve a calcularse desde el startDate del proyecto.'),
2456
+ fixedStartReason: z.string().optional().describe('Motivo de mover la fecha de inicio (obligatorio al mandar fixedStartDate; la API guarda la fecha previa en fixedStartPrev)'),
2457
+ milestones: PHASE_MILESTONES,
2458
+ acceptanceCriteria: PHASE_CRITERIA,
2459
+ }
2460
+ const { name: _phaseName, ...PHASE_FIELDS_OPTIONAL } = PHASE_FIELDS
2461
+
2462
+ registerRead('list_phases', '/v1/phases',
2463
+ 'Lista las fases del plan del proyecto (ordenadas) con sus hitos y criterios de aceptación. Cada fase trae fechas calculadas: effectiveStartDate, effectiveEndDate, dateIsFixed (true si tiene fecha fija movida a mano) y milestones[].effectiveDate. Se calculan desde el startDate del proyecto (update_project) + offsetDays; si el proyecto no tiene startDate no hay fechas efectivas. Úsala antes de create_phase/update_phase para no duplicar y para leer los hitos/criterios completos que vas a reenviar.',
2464
+ {}, (args, projectId) => ({ projectId }))
2465
+
2466
+ registerRead('list_members', '/v1/members',
2467
+ 'Lista los miembros del workspace (personas y agentes) con su rol de permisos (role: admin | collaborator | readonly) y su rol funcional (functionalRole: manager | product_owner | developer | reviewer, null si no tiene), que solo decide qué ve cada quien al entrar y no es un permiso. Úsala para saber quién gestiona, aprueba o revisa antes de asignar tareas o verificaciones.',
2468
+ {}, (args, projectId) => ({ projectId }))
2469
+
2470
+ registerCreate('create_phase', '/v1/phases',
2471
+ 'Crea una fase del plan de un proyecto de Orquestra (ej. "Diseño", "Beta cerrada"). Sus fechas se calculan desde el startDate del proyecto + offsetDays + durationDays; para una fecha fija usa fixedStartDate con fixedStartReason. Revisa list_phases primero para no duplicar.',
2472
+ PHASE_FIELDS)
2473
+
2474
+ registerUpdate('update_phase', '/v1/phases',
2475
+ 'Edita una fase del plan: nombre, orden, duración, estatus (pending | active | done), hitos y criterios de aceptación. Para mover su fecha manda fixedStartDate + fixedStartReason (obligatorio, queda registrado con la fecha previa); fixedStartDate null la devuelve al cálculo automático. milestones y acceptanceCriteria REEMPLAZAN la lista completa: para marcar uno hecho, lee list_phases y reenvía todos con sus id cambiando solo done.',
2476
+ PHASE_FIELDS_OPTIONAL)
2477
+
2478
+ server.registerTool(
2479
+ 'delete_phase',
2480
+ {
2481
+ description: 'Borra una fase del plan del proyecto. Es un borrado real: úsalo solo si la fase fue un error o el usuario lo pidió; para una fase que ya terminó usa update_phase con status "done".',
2482
+ inputSchema: {
2483
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
2484
+ phaseId: z.string().describe('ID de la fase a borrar (ver list_phases)'),
2485
+ },
2486
+ },
2487
+ async ({ projectId, phaseId }) => {
2488
+ try {
2489
+ return textResult(await apiDelete(config, '/v1/phases', { projectId: resolveProjectId(projectId), id: phaseId }))
2490
+ } catch (err) {
2491
+ return errorResult(err)
2492
+ }
2493
+ }
2494
+ )
2495
+
2419
2496
  registerCreate('create_status', '/v1/statuses', 'Crea un estatus (columna del flujo de tareas) en un proyecto.', {
2420
2497
  name: z.string().describe('Nombre del estatus'),
2421
2498
  color: z.string().describe('Color hex, ej: #5E6AD2'),
@@ -2916,7 +2993,7 @@ export function createServer() {
2916
2993
  server.registerTool(
2917
2994
  'update_project',
2918
2995
  {
2919
- description: 'Edita el proyecto configurado (name, description, phase, projectType, scale, url). Útil para sincronizar la fase real de un proyecto que ya estaba avanzado antes de conectarlo a Orquestra -- get_project_context puede verse "vacío" (sin módulos/tareas) sin que eso signifique que el proyecto recién empieza.',
2996
+ description: 'Edita el proyecto configurado (name, description, phase, projectType, scale, url, startDate -- ancla del plan de fases). Útil para sincronizar la fase real de un proyecto que ya estaba avanzado antes de conectarlo a Orquestra -- get_project_context puede verse "vacío" (sin módulos/tareas) sin que eso signifique que el proyecto recién empieza.',
2920
2997
  inputSchema: {
2921
2998
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
2922
2999
  name: z.string().optional(),
@@ -2925,6 +3002,7 @@ export function createServer() {
2925
3002
  projectType: z.enum(['code', 'content', 'hybrid']).optional(),
2926
3003
  scale: z.enum(['landing', 'app', 'platform']).optional().describe('Eje ORTOGONAL a projectType, no lo reemplaza -- "landing" es una página estática sin backend/auth/db propios (ej. un portafolio o sitio de marketing): marcalo así si el proyecto es claramente eso, para que las sugerencias de setup dejen de proponer backend/auth/database. Default implícito "app" si no se manda -- sin este campo, el comportamiento es igual que antes.'),
2927
3004
  url: z.string().optional(),
3005
+ startDate: z.string().optional().describe('YYYY-MM-DD, fecha ancla del plan: las fases (list_phases) calculan sus fechas efectivas a partir de ella con offsetDays. "" la quita.'),
2928
3006
  },
2929
3007
  },
2930
3008
  async ({ projectId, ...body }) => {