---
name: agenda-reuniones-con-tiempo-limitado
description: Convierte una lista aproximada de temas de reunión en una agenda con tiempos asignados, horas de inicio y fin, responsables y un resultado claro para cada punto (informar, discutir, decidir, lluvia de ideas, revisar), comprueba que encaja en la franja con un margen, señala problemas de agenda como decisiones tardías o apresuradas, demasiadas actualizaciones de estado, puntos sobredimensionados y falta de descansos, y redacta una agenda lista para enviar y una lectura previa. Úsalo cuando el usuario pida "ayúdame a planificar esta reunión", "¿encajará esta agenda en una hora?", "haz esta agenda más ajustada", o comparta una lista de temas para una reunión de equipo, taller, retiro o revisión.
---
# Agenda de reuniones con tiempo limitado
Ayudas a las personas a dirigir reuniones que terminan a tiempo y producen decisiones. Conviertes una lista de temas en un horario realista, das a cada punto un responsable y un propósito, sacas las actualizaciones de estado de la sala y haces visible la matemática del tiempo para que nadie descubra en el minuto 55 que la decisión principal no tiene tiempo restante.
## Archivos en esta habilidad
- `scripts/timebox_agenda.py` - programa cada punto, comprueba el ajuste contra la franja y el margen, señala problemas de agenda y puede ajustar los puntos a la franja en pasos de 5 minutos (solo biblioteca estándar de Python 3)
- `references/timeboxing-principles.md` - cuánto tiempo toman realmente las cosas, resultados de los puntos, orden, márgenes y descansos
- `references/facilitation-moves.md` - técnicas para mantener cada tipo de punto dentro de su tiempo asignado y qué hacer cuando uno se excede
- `templates/agenda.md` - el formato de entrada de la agenda y la agenda para enviar
- `examples/example-roadmap-meeting.md` - una corrección trabajada de una reunión de planificación de una hora sobrecargada
## Flujo de trabajo
### 1. Recopila los datos
Pregunta, o asume y dilo:
- Propósito de la reunión en una frase: ¿qué debe ser cierto cuando termine?
- Hora de inicio y duración (o hora de fin), número de asistentes, presencial o remoto.
- Temas, cada uno con un responsable y lo que el grupo debe hacer con él: informar, actualizar, discutir, lluvia de ideas, revisar o decidir.
- Restricciones duras: personas que se unen tarde o se van temprano, descansos fijos.
### 2. Redacta el borrador de la agenda
Usa el formato de entrada en `templates/agenda.md` (una línea por punto: `minutos | tema | responsable | resultado`). Usa `?` para una duración que nadie conoce todavía. Estima las duraciones con `references/timeboxing-principles.md`.
### 3. Ejecuta el limitador de tiempo
```bash
python3 scripts/timebox_agenda.py agenda.txt
python3 scripts/timebox_agenda.py agenda.txt --fit # escala a la franja en pasos de 5 minutos
python3 scripts/timebox_agenda.py agenda.json --json
```
Imprime el horario con las horas de reloj, el margen y los hallazgos: ALTO cuando la agenda no encaja, ADVERTENCIA para problemas, INFO para tiempo no planificado y el costo de la reunión en horas-persona. `--fit` recorta primero los puntos de estado, luego escala todo lo demás; nunca reordena los puntos, así que comprueba el orden tú mismo después.
Si no puedes ejecutar el script, haz la matemática del reloj a mano, punto por punto, y dilo.
### 4. Arregla la agenda, no solo los números
Trabaja los hallazgos en este orden:
1. Elimina o reduce las actualizaciones de estado y anuncios: muévelos a una lectura previa escrita.
2. Pon las decisiones temprano (primera mitad) y da a cada una al menos 10 minutos más una propuesta escrita enviada de antemano.
3. Da a cada punto un responsable y un resultado; elimina "Varios" o conviértelo en una revisión de aparcamiento de 5 minutos.
4. Divide los puntos de más de 45 minutos; añade un descanso cada 60 a 90 minutos.
5. Si todavía no encaja, propón qué recortar o mover a otra reunión o a asíncrono, y di qué se pierde.
Vuelve a ejecutar el script hasta que no haya hallazgos ALTOS y cada ADVERTENCIA esté arreglada o explicada.
### 5. Entrega
Completa la agenda para enviar desde `templates/agenda.md`: propósito, resultados deseados, el horario con tiempos, lecturas previas con responsables y plazos, y la regla del aparcamiento. Añade uno o dos consejos de facilitación por punto arriesgado de `references/facilitation-moves.md`. Sigue el estilo de `examples/example-roadmap-meeting.md`.
## Reglas
- Nunca hagas que la matemática funcione reduciendo una decisión por debajo de lo que necesita; recorta o mueve otra cosa, o recomienda una franja más larga.
- Mantén un margen: unos 5 minutos por hora, 10 por ciento para talleres.
- Di claramente cuando una reunión no debería ocurrir (sin decisiones, nada que discutir) y sugiere la alternativa asíncrona.
- Respeta la cultura y las restricciones del usuario; sugiere, no sermonees.
- Las horas son locales a la reunión; para asistentes remotos en otras zonas horarias, menciónalo pero no conviertas a menos que se pida.
## Principios de limitación de tiempo
### Cada punto necesita un resultado
| Resultado | Lo que hace el grupo | Tiempo típico | Notas |
|---|---|---|---|
| informar | Escuchar algo, hacer una pregunta aclaratoria | 2 a 5 min | Mejor como lectura previa escrita. |
| actualizar | Estado de personas o equipos | 1 a 2 min por persona | Generalmente asíncrono; mantén solo los bloqueos en vivo. |
| discutir | Explorar un tema, compartir opiniones, sin decisión todavía | 10 a 20 min | Di a qué alimenta la discusión (una decisión la próxima semana, un documento). |
| lluvia de ideas | Generar opciones | 15 a 30 min | Necesita una pregunta enmarcada y un método (escritura silenciosa primero). |
| revisar | Dar retroalimentación sobre un borrador, diseño o plan | 15 a 40 min | Envía el material de antemano; acuerda qué tipo de retroalimentación se quiere. |
| decidir | Elegir entre opciones y comprometerse | 10 a 30 min | Necesita una propuesta escrita, las opciones, el tomador de decisiones y la regla de decisión. |
| descanso | Descansar | 5 a 15 min | Cada 60 a 90 minutos. |
Si un punto no tiene un resultado claro, pregunta: "¿Qué será diferente después de este punto?" Si la respuesta es "nada", elimínalo.
### Cuánto tiempo toman realmente las cosas
- Presentaciones: alrededor de 1 minuto por persona para un grupo nuevo, 0 para un equipo regular.
- Cada persona hablando una vez sobre un tema: alrededor de 1.5 a 2 minutos por persona. Ocho personas, una ronda, son 15 minutos.
- Compartir pantalla y empezar: 2 a 3 minutos, más para reuniones remotas con herramientas nuevas.
- Una decisión con desacuerdo real: duplica la estimación, o divídela en "discutir" ahora y "decidir" la próxima vez.
- Los puntos al final de la agenda van más rápido pero peor: la gente se apresura o se va.
### Orden
1. Empieza con un encuadre breve: propósito y resultados deseados (2 a 5 min).
2. Decisiones y los temas más difíciles en la primera mitad, mientras la energía y la asistencia son altas.
3. Discusión y revisión en el medio.
4. Información y actualizaciones al final o fuera de la reunión por completo.
5. Termina con próximos pasos, responsables y fechas (5 min), nunca con "Varios".
Agrupa los puntos que necesitan a las mismas personas, para que otros puedan irse temprano.
### Márgenes y descansos
- Margen: unos 5 minutos por hora, 10 por ciento para talleres. El margen es parte de la franja, no tiempo extra.
- Descansos: 5 a 10 minutos cada 60 a 90 minutos; 15 para talleres más largos. Las reuniones remotas necesitan descansos antes.
- Calendarios consecutivos: termina 5 minutos antes (reuniones de 25 o 50 minutos).
### Comprobaciones de ajuste
- Más de unos 8 puntos por hora significa que cada uno recibe menos de 7 minutos; fusiona o recorta.
- Estado y anuncios por encima de un tercio del tiempo: muévelos a una lectura previa.
- Un punto de más de 45 minutos: divídelo en pasos con sus propios resultados (por ejemplo, "lluvia de ideas 25" luego "agrupar y elegir 20").
### El costo de una reunión
Horas-persona = asistentes x duración en horas. Una reunión de una hora con 9 personas cuesta 9 horas de trabajo. Pregunta si todos necesitan estar allí para cada punto.
## Movimientos de facilitación que mantienen los tiempos
### Antes de la reunión
- Envía la agenda y las lecturas previas 24 a 48 horas antes, con el responsable de cada punto.
- Para cada punto de decisión, envía una propuesta de una página: la pregunta, las opciones, la recomendación y quién decide.
- Pide a la gente que añada preguntas a la lectura previa por escrito, para que la reunión empiece desde las preguntas, no desde una presentación.
### Al inicio
- Declara el propósito y los resultados que necesitas para el final.
- Nombra un cronometrador y acuerda la señal (un mensaje en el chat, una mano levantada a 2 minutos del final).
- Di cómo se toman las decisiones hoy: el responsable decide después de la aportación, voto mayoritario o consentimiento ("¿alguna objeción fuerte?").
### Por tipo de punto
- Informar: "Dos minutos, luego una pregunta aclaratoria cada uno". Las preguntas detalladas van al aparcamiento o a un chat de seguimiento.
- Actualizar: Solo bloqueos y peticiones en vivo; todo lo demás está en la lectura previa.
- Discutir: Enmarca la pregunta primero ("¿Qué nos preocupa de la opción B?"). Usa una ronda rápida para que todos hablen una vez antes de que alguien hable dos veces.
- Lluvia de ideas: 3 a 5 minutos de escritura silenciosa primero, luego compartir, luego agrupar. Cantidad antes que juicio.
- Revisar: Acuerda el tipo de retroalimentación: problemas bloqueantes, preguntas o pulido. Recoge los problemas bloqueantes primero.
- Decidir: Replantea la propuesta, enumera las preocupaciones, comprueba las objeciones, luego decide y dilo en voz alta: "Hemos decidido X; responsable Y; para la fecha Z".
### Cuando un punto se excede
Haz visible la elección en lugar de comerte silenciosamente el siguiente punto:
1. "Nos quedan 2 minutos en esto. ¿Ampliamos 5 minutos y recortamos otra cosa, o lo aparcamos?"
2. Aparca: escríbelo en el aparcamiento con un responsable y una fecha.
3. Reduce el siguiente punto, nunca los puntos de decisión.
4. Si el objetivo de la reunión está en riesgo, programa un seguimiento breve solo con las personas necesarias.
### Remoto e híbrido
- Todos se unen con su propio dispositivo para que los remotos sean iguales.
- Usa el chat para la cola de turnos de palabra y para encuestas rápidas.
- Comparte la agenda en pantalla con el punto actual resaltado.
### Al final
- Lee en voz alta las decisiones, responsables y fechas (5 minutos).
- Revisa el aparcamiento: asigna cada punto o elimínalo.
- Envía las notas dentro de 24 horas.
## Entrada de agenda (para scripts/timebox_agenda.py)
```
Reunión: <nombre>
Inicio: <HH:MM>
Duración: <minutos> # o Fin: <HH:MM>
Margen: <minutos o porcentaje, ej. 5 o 10%>
Asistentes: <número>
<minutos> | <tema> | <responsable> | <informar | actualizar | discutir | lluvia de ideas | revisar | decidir | descanso>
? | <tema con duración desconocida> | <responsable> | <resultado>
```
Una tabla Markdown con las columnas Min, Tema, Responsable, Resultado también funciona, y también JSON:
`{"meeting": "...", "start": "14:00", "length": 60, "buffer": 5, "attendees": 6, "items": [{"minutes": 10, "topic": "...", "owner": "...", "outcome": "decide"}]}`
---
# Agenda para enviar
**<Nombre de la reunión>**
<Día, fecha>, <inicio>-<fin> (<duración> min), <lugar o enlace de video>
Asistentes: <nombres o equipos> | Facilitador: <nombre> | Cronometrador: <nombre> | Notas: <nombre>
**Propósito:** <una frase: qué debe ser cierto cuando terminemos>
**Resultados que necesitamos**
1. <decisión o resultado>
2. <decisión o resultado>
**Lectura previa (para <día, hora>)**
- <documento> - <responsable> - <qué mirar, cuánto tiempo toma leerlo>
**Horario**
| Hora | Min | Punto | Responsable | Resultado |
|---|---|---|---|---|
| <HH:MM-HH:MM> | <n> | <tema> | <responsable> | <resultado> |
| <HH:MM-HH:MM> | <n> | Margen y cierre: decisiones, responsables, fechas | <facilitador> | |
**Cómo decidimos hoy:** <el responsable decide después de la aportación | consentimiento | voto>
**Aparcamiento:** Los puntos fuera de tema van al aparcamiento y reciben un responsable al final.
**Movido fuera de esta reunión**
- <punto> -> <actualización asíncrona, otra reunión o un documento>, responsable <nombre>
## Ejemplo: arreglar una reunión de planificación de una hora sobrecargada
### La solicitud
"Tengo una hora con 9 personas el jueves a las 14:00 para planificar el Q4. Aquí está mi agenda. ¿Encajará? La división del presupuesto debe aprobarse en esta reunión, y las 3 iniciativas principales también."
Borrador de la agenda (`agenda.txt`):
```
Reunión: Planificación de la hoja de ruta Q4
Inicio: 14:00
Duración: 60
Margen: 5
Asistentes: 9
10 | Bienvenida y objetivos | Dana | informar
15 | Ronda de estado del equipo (cada líder de equipo) | Líderes | actualizar
10 | Destacados de comentarios de clientes | Priya | informar
20 | Prioridades Q4: elegir las 3 iniciativas principales | Lee | discutir
10 | Plan de contratación para Q4 | | discutir
5 | Aprobar la división del presupuesto Q4 | Dana | decidir
? | Varios | |
```
### Primera ejecución
```
$ python3 scripts/timebox_agenda.py agenda.txt
Planificación de la hoja de ruta Q4: 14:00-15:00 (60 min, margen 5 min, 55 min para puntos)
Planificado: 70 min
14:00-14:10 10 min Bienvenida y objetivos (Dana) [informar]
14:10-14:25 15 min Ronda de estado del equipo (cada líder de equipo) (Líderes) [actualizar]
14:25-14:35 10 min Destacados de comentarios de clientes (Priya) [informar]
14:35-14:55 20 min Prioridades Q4: elegir las 3 iniciativas principales (Lee) [discutir]
14:55-15:05 10 min Plan de contratación para Q4 [discutir]
15:05-15:10 5 min Aprobar la división del presupuesto Q4 (Dana) [decidir]
15:10-15:10 ? min Varios [?]
14:55-15:00 5 min Margen / cierre
Hallazgos:
- [ALTO] La agenda necesita 70 min pero solo hay 55 min disponibles después del margen de 5 min: exceso de 15 min (termina a las 15:10 en lugar de las 14:55). Recorta, acorta o mueve puntos, o ejecuta con --fit.
- [ADVERTENCIA] 1 punto(s) sin duración: Varios. Tiempo restante para ellos: 0 min.
- [ADVERTENCIA] "Plan de contratación para Q4" no tiene responsable; nombra quién lo prepara y lo dirige.
- [ADVERTENCIA] "Aprobar la división del presupuesto Q4" es una decisión con solo 5 min; planifica al menos 10, o decide con una propuesta escrita de antemano.
- [ADVERTENCIA] "Aprobar la división del presupuesto Q4" es una decisión que comienza a las 15:05, en el último cuarto de la reunión; muévela antes mientras la energía y la asistencia son altas.
- [ADVERTENCIA] "Varios" no tiene responsable; nombra quién lo prepara y lo dirige.
- [ADVERTENCIA] "Varios" no tiene resultado; di si es para informar, discutir, decidir o lluvia de ideas.
- [ADVERTENCIA] Las actualizaciones de estado y anuncios toman el 50% del tiempo; envíalos como lectura previa y guarda la reunión para discusión y decisiones.
- [INFO] Costo de la reunión: 9 personas x 60 min = 9 horas-persona.
```
Código de salida 1: la agenda no encaja.
### Qué dicen los números
- 70 minutos de puntos (más Varios) en una ventana de 55 minutos: 15 minutos de exceso antes de que Varios siquiera comience.
- La mitad del tiempo son actualizaciones y anuncios, mientras que las dos cosas que deben decidirse reciben 5 minutos (presupuesto) y una etiqueta de "discutir" (prioridades), y la decisión del presupuesto comenzaría a las 15:05, después de que la reunión debería haber terminado.
- Ejecutar `--fit` solo exprimiría todo en 55 minutos pero dejaría la decisión del presupuesto al final con 5 minutos. Ajustar los números no es suficiente; el orden y el propósito de los puntos tienen que cambiar.
### Cambios
1. La ronda de estado del equipo (15 min) y los destacados de comentarios de clientes (10 min) se convierten en una lectura previa escrita con fecha límite el miércoles a las 12:00; las preguntas van en los comentarios.
2. La división del presupuesto se mueve al inicio con 10 minutos y una propuesta enviada el lunes, para que la reunión solo la confirme o la ajuste.
3. "Prioridades Q4" se convierte en una decisión con 25 minutos: el resultado es un top 3 clasificado.
4. El plan de contratación recibe un responsable (Priya) y una pregunta más estrecha: qué puestos abrir.
5. "Varios" se reemplaza por un cierre de 5 minutos de "próximos pasos, responsables y aparcamiento".
6. Tres personas que solo necesitaban la ronda de estado pueden saltarse la reunión: 6 asistentes en lugar de 9.
Agenda revisada:
```
Reunión: Planificación de la hoja de ruta Q4
Inicio: 14:00
Duración: 60
Margen: 5
Asistentes: 6
5 | Bienvenida y objetivos de hoy | Dana | informar
10 | Aprobar la división del presupuesto Q4 (propuesta enviada el lunes) | Dana | decidir
25 | Prioridades Q4: elegir las 3 iniciativas principales | Lee | decidir
10 | Plan de contratación para Q4: acordar qué puestos abrir | Priya | discutir
5 | Próximos pasos, responsables y aparcamiento | Dana | informar
```
### Segunda ejecución
```
$ python3 scripts/timebox_agenda.py agenda-revisada.txt
Planificación de la hoja de ruta Q4: 14:00-15:00 (60 min, margen 5 min, 55 min para puntos)
Planificado: 55 min
14:00-14:05 5 min Bienvenida y objetivos de hoy (Dana) [informar]
14:05-14:15 10 min Aprobar la división del presupuesto Q4 (propuesta enviada el lunes) (Dana) [decidir]
14:15-14:40 25 min Prioridades Q4: elegir las 3 iniciativas principales (Lee) [decidir]
14:40-14:50 10 min Plan de contratación para Q4: acordar qué puestos abrir (Priya) [discutir]
14:50-14:55 5 min Próximos pasos, responsables y aparcamiento (Dana) [informar]
14:55-15:00 5 min Margen / cierre
Hallazgos:
- [INFO] Costo de la reunión: 6 personas x 60 min = 6 horas-persona.
```
Código de salida 0, sin advertencias, y la reunión cuesta 6 horas-persona en lugar de 9.
### La agenda para enviar (plantilla completada, abreviada)
**Planificación de la hoja de ruta Q4**
Jueves, 14:00-15:00 (60 min), Sala 3 y enlace de video
Facilitadora: Dana | Cronometrador: Lee | Notas: Priya
**Propósito:** Salir con una división del presupuesto Q4 aprobada y un top 3 de iniciativas clasificado.
**Lectura previa (para el miércoles a las 12:00)**
- Resúmenes de una página del estado del equipo - cada líder de equipo - 5 min de lectura
- Destacados de comentarios de clientes - Priya - 5 min
- Propuesta de división del presupuesto - Dana - 10 min
**Horario**
| Hora | Min | Punto | Responsable | Resultado |
|---|---|---|---|---|
| 14:00-14:05 | 5 | Bienvenida y objetivos de hoy | Dana | informar |
| 14:05-14:15 | 10 | Aprobar la división del presupuesto Q4 (propuesta enviada el lunes) | Dana | decidir |
| 14:15-14:40 | 25 | Prioridades Q4: elegir las 3 iniciativas principales | Lee | decidir |
| 14:40-14:50 | 10 | Plan de contratación para Q4: acordar qué puestos abrir | Priya | discutir |
| 14:50-14:55 | 5 | Próximos pasos, responsables y aparcamiento | Dana | informar |
| 14:55-15:00 | 5 | Margen | | |
**Cómo decidimos hoy:** Dana decide el presupuesto después de la aportación; el top 3 se elige por consentimiento (¿alguna objeción fuerte?).
**Consejos de facilitación**
- Prioridades: 3 minutos de votación con puntos silenciosa sobre la lista de candidatos, luego discute solo los puntos clasificados del 2 al 5.
- Si el punto del presupuesto llega a los 10 minutos, aparca el punto abierto con un responsable y un plazo de 24 horas en lugar de comerte las prioridades.
**Movido fuera de esta reunión:** ronda de estado del equipo y comentarios de clientes (lectura previa).
### Por qué esta es una buena respuesta
- Arregla el propósito y el orden, no solo los minutos.
- Ambas decisiones requeridas ahora ocurren en los primeros 40 minutos, con propuestas preparadas.
- El script confirma el ajuste, y las personas que solo estaban allí para las actualizaciones recuperan su hora.
## Script de Python: timebox_agenda.py
```python
#!/usr/bin/env python3
"""Limita el tiempo de una agenda de reunión: programa cada punto, comprueba el ajuste y señala problemas de agenda.
Uso:
python3 timebox_agenda.py agenda.txt
python3 timebox_agenda.py agenda.txt --fit # escala los puntos para que encajen en la franja (pasos de 5 minutos)
python3 timebox_agenda.py agenda.json --json
cat agenda.txt | python3 timebox_agenda.py -
Entrada de texto: líneas de cabecera opcionales "Clave: valor", luego un punto por línea como
minutos | tema | responsable | resultado
Las filas de tabla Markdown (| 10 | Tema | Responsable | decidir |) también funcionan; las filas de cabecera y
separador se omiten. Usa ? para una duración desconocida.
Claves de cabecera: Reunión, Inicio (HH:MM), Duración (minutos) o Fin (HH:MM),
Margen (minutos o porcentaje, ej. 5 o 10%), Asistentes (número).
Resultados: informar, actualizar, discutir, lluvia de ideas, revisar, decidir, descanso.
Entrada JSON: {"meeting": "...", "start": "14:00", "length": 60, "buffer": 5,
"attendees": 8, "items": [{"minutes": 10, "topic": "...", "owner": "...", "outcome": "decide"}]}
--fit recorta primero los puntos de estado (y los puntos sin resultado), luego escala todo; el tiempo sobrante va a
los puntos de decidir y discutir.
Comprobaciones: exceso (ALTO), duraciones, responsables o resultados faltantes, decisiones tardías o
apresuradas, puntos sobredimensionados, tramos largos sin descanso, demasiadas actualizaciones
de estado, demasiados puntos por hora, más el costo de la reunión en horas-persona.
Código de salida: 0 sin hallazgos ALTOS, 1 ALTO encontrado, 2 error de uso o entrada.
Solo biblioteca estándar.
"""
import argparse
import json
import math
import re
import sys
OUTCOMES = {"informar", "actualizar", "discutir", "lluvia de ideas", "revisar", "decidir", "descanso"}
ALIASES = {"decision": "decidir", "info": "informar", "fyi": "informar", "estado": "actualizar", "actualizaciones": "actualizar",
"discusion": "discutir", "ideas": "lluvia de ideas", "ideacion": "lluvia de ideas", "retroalimentacion": "revisar",
"aprobar": "decidir", "aprobacion": "decidir", "pausa": "descanso", "cafe": "descanso"}
class InputError(Exception):
pass
def hhmm(text):
m = re.fullmatch(r"\s*(\d{1,2})[:.](\d{2})\s*", str(text))
if not m or int(m.group(1)) > 23 or int(m.group(2)) > 59:
raise InputError(f"la hora debe ser HH:MM, se obtuvo {text!r}")
return int(m.group(1)) * 60 + int(m.group(2))
def fmt(minutes):
minutes = int(round(minutes)) % (24 * 60)
return f"{minutes // 60:02d}:{minutes % 60:02d}"
def parse_minutes(text):
t = str(text).strip().lower()
if t in ("", "?", "tbd", "-"):
return None
m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(m|min|mins|minutes?)?", t)
if m:
return float(m.group(1))
m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*h(?:ours?)?(?:\s*(\d+)\s*m(?:in)?)?", t)
if m:
return float(m.group(1)) * 60 + float(m.group(2) or 0)
raise InputError(f"no se puede leer la duración {text!r}")
def norm_outcome(text):
t = (text or "").strip().lower()
return ALIASES.get(t, t)
def parse_text(text):
meta, items, errors = {}, [], []
for no, raw in enumerate(text.replace("\r\n", "\n").split("\n"), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
hm = re.fullmatch(r"([A-Za-z ]{3,20}):\s*(.+)", line)
if hm and "|" not in line:
meta[hm.group(1).strip().lower()] = hm.group(2).strip()
continue
if "|" not in line:
errors.append(f"línea {no}: se esperaba 'minutos | tema | responsable | resultado', se obtuvo {line[:60]!r}")
continue
cells = [c.strip() for c in line.strip("|").split("|")]
if all(re.fullmatch(r":?-{2,}:?", c) for c in cells if c):
continue
if cells and cells[0].lower() in ("min", "mins", "minutes", "time", "duration"):
continue
cells += [""] * (4 - len(cells))
try:
minutes = parse_minutes(cells[0])
except InputError as e:
errors.append(f"línea {no}: {e}")
continue
if not cells[1]:
errors.append(f"línea {no}: el punto no tiene tema")
continue
items.append({"line": no, "minutes": minutes, "topic": cells[1], "owner": cells[2],
"outcome": norm_outcome(cells[3])})
return meta, items, errors
def parse_json(text):
data = json.loads(text)
if not isinstance(data, dict) or not isinstance(data.get("items"), list):
raise InputError("la entrada JSON necesita un objeto con una lista 'items'")
meta = {k.lower(): str(v) for k, v in data.items() if k != "items" and v is not None}
items, errors = [], []
for i, it in enumerate(data["items"], 1):
try:
minutes = parse_minutes(it.get("minutes", ""))
except InputError as e:
errors.append(f"punto {i}: {e}")
continue
if not str(it.get("topic", "")).strip():
errors.append(f"punto {i}: el punto no tiene tema")
continue
items.append({"line": i, "minutes": minutes, "topic": str(it["topic"]).strip(),
"owner": str(it.get("owner", "") or "").strip(), "outcome": norm_outcome(it.get("outcome", ""))})
return meta, items, errors
def settings(meta):
start = hhmm(meta.get("start", "09:00"))
if "length" in meta:
length = parse_minutes(meta["length"])
if not length or length <= 0:
raise InputError("La duración debe ser un número positivo de minutos")
elif "end" in meta:
length = (hhmm(meta["end"]) - start) % (24 * 60)
if length == 0:
raise InputError("El fin debe ser después del inicio")
else:
raise InputError("la cabecera necesita 'Duración: <minutos>' o 'Fin: HH:MM'")
b = meta.get("buffer", "0").strip()
if b.endswith("%"):
buffer = 5 * round(length * float(b[:-1]) / 100 / 5) # los márgenes porcentuales se redondean a 5 minutos
else:
buffer = parse_minutes(b) or 0
if buffer >= length:
raise InputError("El margen debe ser más corto que la reunión")
attendees = int(meta["attendees"]) if str(meta.get("attendees", "")).strip().isdigit() else None
return {"meeting": meta.get("meeting", meta.get("title", "Reunión")), "start": start, "length": length,
"buffer": buffer, "available": length - buffer, "attendees": attendees}
def fit(items, available):
"""Ajusta los puntos a `available` minutos en pasos de 5 minutos (las duraciones faltantes cuentan como 10, los descansos se mantienen).
Cuando la agenda es demasiado larga, los puntos de estado (informar, actualizar o sin resultado) se recortan primero, hasta la mitad
de su tiempo planificado; luego cada punto se escala proporcionalmente. Cuando es demasiado corta, el
tiempo sobrante va a los puntos de decidir y discutir primero.
"""
fixed = sum(it["minutes"] or 0 for it in items if it["outcome"] == "descanso")
flex = [it for it in items if it["outcome"] != "descanso"]
room = available - fixed
if not flex or room < 5 * len(flex):
raise InputError(f"no se pueden ajustar {len(flex)} puntos en {room:g} minutos con al menos 5 minutos cada uno")
want = [max(5, 5 * round((it["minutes"] or 10) / 5)) for it in flex]
cur = list(want)
status = [i for i, it in enumerate(flex) if it["outcome"] in ("informar", "actualizar", "")]
while sum(cur) > room:
cands = [i for i in status if cur[i] - 5 >= max(5, want[i] / 2)]
if not cands:
break
cur[max(cands, key=lambda k: cur[k])] -= 5
if sum(cur) > room:
scale = room / sum(cur)
ref = [c * scale for c in cur]
cur = [max(5, 5 * round(r / 5)) for r in ref]
while sum(cur) > room:
cands = [k for k in range(len(cur)) if cur[k] > 5]
cur[max(cands, key=lambda k: (cur[k] - ref[k], cur[k]))] -= 5
while sum(cur) + 5 <= room:
cur[min(range(len(cur)), key=lambda k: cur[k] - ref[k])] += 5
else:
prio = [i for i, it in enumerate(flex) if it["outcome"] in ("decidir", "discutir")] or list(range(len(flex)))
while sum(cur) + 5 <= room:
cur[min(prio, key=lambda k: cur[k] / want[k])] += 5
out, it_new = [], iter(cur)
for it in items:
copy = dict(it)
copy["original"] = it["minutes"]
if it["outcome"] != "descanso":
copy["minutes"] = float(next(it_new))
out.append(copy)
return out
def analyze(items, cfg):
findings = []
def add(sev, msg):
findings.append({"severity": sev, "message": msg})
t = cfg["start"]
for it in items:
it["start"] = t
t += it["minutes"] or 0
it["end"] = t
planned = sum(it["minutes"] or 0 for it in items)
avail, end_slot = cfg["available"], cfg["start"] + cfg["length"]
unknown = [it for it in items if it["minutes"] is None]
if planned > avail:
add("ALTO", f"La agenda necesita {planned:g} min pero solo hay {avail:g} min disponibles"
f"{' después del margen de ' + format(cfg['buffer'], 'g') + ' min' if cfg['buffer'] else ''}: exceso de {planned - avail:g} min"
f" (termina a las {fmt(cfg['start'] + planned)} en lugar de las {fmt(end_slot - cfg['buffer'])}). Recorta, acorta o mueve puntos, o ejecuta con --fit.")
elif avail - planned > 0.15 * avail and not unknown:
add("INFO", f"{avail - planned:g} min no planificados; termina temprano o da a la decisión principal más tiempo.")
if unknown:
add("ADVERTENCIA", f"{len(unknown)} punto(s) sin duración: " + "; ".join(it["topic"][:40] for it in unknown)
+ f". Tiempo restante para ellos: {max(avail - planned, 0):g} min.")
for it in items:
name = f"\"{it['topic'][:50]}\""
if it["outcome"] != "descanso" and not it["owner"]:
add("ADVERTENCIA", f"{name} no tiene responsable; nombra quién lo prepara y lo dirige.")
if not it["outcome"]:
add("ADVERTENCIA", f"{name} no tiene resultado; di si es para informar, discutir, decidir o lluvia de ideas.")
elif it["outcome"] not in OUTCOMES:
add("ADVERTENCIA", f"{name} tiene un resultado desconocido '{it['outcome']}' (usa: {', '.join(sorted(OUTCOMES))}).")
m = it["minutes"] or 0
if it["outcome"] == "decidir":
if it["minutes"] is not None and m < 10:
add("ADVERTENCIA", f"{name} es una decisión con solo {m:g} min; planifica al menos 10, o decide con una propuesta escrita de antemano.")
if it["start"] - cfg["start"] >= 0.75 * cfg["length"]:
add("ADVERTENCIA", f"{name} es una decisión que comienza a las {fmt(it['start'])}, en el último cuarto de la reunión; muévela antes mientras la energía y la asistencia son altas.")
if it["outcome"] in ("discutir", "lluvia de ideas", "revisar") and it["minutes"] is not None and m < 5:
add("ADVERTENCIA", f"{name} es un punto de {it['outcome']} con {m:g} min; eso es una mención, no un {it['outcome']}.")
if m > 45 and it["outcome"] != "descanso":
add("ADVERTENCIA", f"{name} tiene {m:g} min de duración; divídelo en partes con sus propios resultados.")
stretch, stretch_start = 0, cfg["start"]
for it in items:
if it["outcome"] == "descanso":
stretch, stretch_start = 0, it["end"]
continue
stretch += it["minutes"] or 0
if stretch > 90:
add("ADVERTENCIA", f"{stretch:g} min sin descanso desde las {fmt(stretch_start)}; añade un descanso de 5 a 10 min aproximadamente cada 60 a 90 min.")
stretch = -10**9
status = sum(it["minutes"] or 0 for it in items if it["outcome"] in ("informar", "actualizar"))
if planned and status / planned > 0.35:
add("ADVERTENCIA", f"Las actualizaciones de estado y anuncios toman el {round(100 * status / planned)}% del tiempo; envíalos como lectura previa y guarda la reunión para discusión y decisiones.")
work_items = [it for it in items if it["outcome"] != "descanso"]
hours = cfg["length"] / 60
if hours and len(work_items) / hours > 8:
add("ADVERTENCIA", f"{len(work_items)} puntos en {cfg['length']:g} min es más de 8 por hora; fusiona o elimina puntos.")
if not any(it["outcome"] == "decidir" for it in items):
add("INFO", "No hay puntos de decisión; comprueba que esta reunión necesita ser en vivo.")
cost = None
if cfg["attendees"]:
cost = round(cfg["attendees"] * cfg["length"] / 60, 1)
add("INFO", f"Costo de la reunión: {cfg['attendees']} personas x {cfg['length']:g} min = {cost:g} horas-persona.")
order = {"ALTO": 0, "ADVERTENCIA": 1, "INFO": 2}
findings.sort(key=lambda f: order[f["severity"]])
return {"meeting": cfg["meeting"], "start": fmt(cfg["start"]), "end": fmt(end_slot), "length": cfg["length"],
"buffer": cfg["buffer"], "available": avail, "planned": planned, "person_hours": cost,
"items": [{k: (fmt(v) if k in ("start", "end") else v) for k, v in it.items()} for it in items],
"findings": findings}
def main():
ap = argparse.ArgumentParser(description="Limita el tiempo de una agenda de reunión")
ap.add_argument("path", help="archivo de agenda texto/Markdown/JSON, o - para stdin")
ap.add_argument("--fit", action="store_true", help="escala las duraciones de los puntos para ajustarse al tiempo disponible")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
try:
text = sys.stdin.read() if args.path == "-" else open(args.path, encoding="utf-8").read()
if not text.strip():
raise InputError("la entrada está vacía")
meta, items, errors = parse_json(text) if text.lstrip().startswith("{") else parse_text(text)
if errors:
raise InputError("no se pudo leer la agenda:\n " + "\n ".join(errors))
if not items:
raise InputError("no se encontraron puntos de agenda (se esperaban líneas como '10 | Tema | Responsable | decidir')")
cfg = settings(meta)
before = sum(it["minutes"] or 0 for it in items)
n_unknown = sum(1 for it in items if it["minutes"] is None)
if args.fit:
items = fit(items, cfg["available"])
except FileNotFoundError:
print(f"error: archivo no encontrado: {args.path}", file=sys.stderr)
return 2
except (InputError, json.JSONDecodeError, ValueError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
rep = analyze(items, cfg)
rep["fitted_from"] = before if args.fit else None
if args.json:
print(json.dumps(rep, indent=2))
else:
print(f"{rep['meeting']}: {rep['start']}-{rep['end']} ({rep['length']:g} min, margen {rep['buffer']:g} min, {rep['available']:g} min para puntos)")
if args.fit:
extra = f" más {n_unknown} sin duración (contados como 10 min cada uno)" if n_unknown else ""
print(f"Ajustado: {before:g} min de puntos{extra} ajustados en {rep['planned']:g} min (pasos de 5 minutos, descansos mantenidos).")
print(f"Planificado: {rep['planned']:g} min\n")
for it in rep["items"]:
mins = "?" if it["minutes"] is None else f"{it['minutes']:g}"
was = f" (era {it['original']:g})" if args.fit and it.get("original") not in (None, it["minutes"]) else ""
who = f" ({it['owner']})" if it["owner"] else ""
print(f" {it['start']}-{it['end']} {mins:>3} min{was:<10} {it['topic']}{who} [{it['outcome'] or '?'}]")
if rep["buffer"]:
print(f" {fmt(hhmm(rep['end']) - rep['buffer'])}-{rep['end']} {rep['buffer']:>3g} min Margen / cierre")
print()
if rep["findings"]:
print("Hallazgos:")
for f in rep["findings"]:
print(f"- [{f['severity']}] {f['message']}")
else:
print("Hallazgos: ninguno")
return 1 if any(f["severity"] == "ALTO" for f in rep["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
```
Discusión
0 comentarios