product-update-logger
Indica a esta habilidad lo que tu producto ha lanzado. Escribe una entrada pulida de changelog en docs/changelog.md (un registro vivo, con la entrada más nueva primero) y produce simultáneamente un paquete de contenido: hilo de tweets, publicación de LinkedIn, fragmento de correo electrónico y una frase corta.
Fuentes de entrada: texto libre de tu mensaje, commits de git leídos automáticamente del repositorio local, o PRs de GitHub si proporcionas un repositorio. Cualquier combinación funciona.
Archivos de Referencia
Lee estos archivos antes de cada ejecución:
cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md
Paso 1: Verificación de Configuración
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-no configurado -- obtención de PRs de GitHub deshabilitada}"
echo "Git: $(git rev-parse --is-inside-work-tree 2>/dev/null && echo 'repositorio detectado' || echo 'no es un repositorio git')"
echo "Changelog: $(ls docs/changelog.md 2>/dev/null && echo 'existe' || echo 'se creará')"
Nota si git está disponible y si ya existe un changelog. Esto determina el formato de la etiqueta de versión.
Paso 2: Analizar la Entrada
Recopila de la conversación:
items-- descripción en texto libre de lo que se lanzó (separado por tuberías|si hay varios). Opcional si git está disponible.since-- cuántos días hacia atrás mirar. Por defecto: 7. El usuario puede decir "últimas 2 semanas" (14) o "desde el último lanzamiento".repo-- "propietario/repositorio" de GitHub para obtener PRs. Opcional.version_label-- etiqueta personalizada como "v2.1.0" o "La Actualización de Velocidad". Opcional; por defecto es basada en la fecha.
Si el usuario no dijo nada sobre los items Y no hay repositorio git: Pregunta "¿Qué lanzaste? Enumera las características, correcciones o mejoras -- una por línea."
Si git está disponible y el usuario no dijo nada específico: Procede con la lectura automática de git en el Paso 3. Muestra al usuario lo que se encontró y confirma antes de transformar.
Escribe la entrada analizada:
python3 << 'PYEOF'
import json, os, re
inp = {
"items": "", # LLENAR: texto libre separado por tuberías, o "" si no hay
"since": 7, # LLENAR: número entero de días
"repo": "", # LLENAR: "propietario/repositorio" o ""
"version_label": "" # LLENAR: "" significa automático (basado en fecha), o cadena personalizada
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"Desde hace: {inp['since']} días")
print(f"Items de texto libre: {inp['items'] or 'ninguno (se usará git/GitHub)'}")
print(f"Repositorio GitHub: {inp['repo'] or 'ninguno'}")
print(f"Etiqueta de versión: {inp['version_label'] or 'automática (basada en fecha)'}")
PYEOF
Paso 3: Ejecutar el Script de Recopilación
ls scripts/gather.py 2>/dev/null && echo "script encontrado" || echo "ERROR: scripts/gather.py no encontrado"
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/gather.py \
--since "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['since'])")" \
--repo "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['repo'])")" \
--items "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['items'])")" \
--output /tmp/pul-raw.json
Verifica la salida:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(f'Items encontrados: {d[\"total_items\"]}')
print(f'Ruido filtrado: {d[\"noise_filtered\"]}')
print(f'Git disponible: {d[\"git_available\"]}')
print(f'GitHub disponible: {d[\"github_available\"]}')
print(f'Fuentes: git={sum(1 for i in d[\"items\"] if i[\"source\"]==\"git_commit\")}, '
f'prs={sum(1 for i in d[\"items\"] if i[\"source\"]==\"github_pr\")}, '
f'texto={sum(1 for i in d[\"items\"] if i[\"source\"]==\"free_text\")}')
print()
print('Items:')
for item in d['items']:
print(f' [{item[\"source\"]}] {item[\"subject\"]}')
"
Si total_items == 0: Detente. Dile al usuario: "No se encontraron items lanzados. Describe lo que lanzaste, apúntame a un repositorio git con commits recientes, o añade un repositorio de GitHub con repo: propietario/repositorio y un GITHUB_TOKEN."
Muestra la lista de items al usuario y pregunta: "Estos son los items que encontré. ¿Algo que añadir o eliminar antes de que escriba el changelog?"
Espera confirmación o ediciones. Si el usuario dice "todo bien", "adelante", o no hace cambios, continúa. Si el usuario añade o elimina items, actualiza /tmp/pul-raw.json en consecuencia antes del Paso 4.
Paso 4: Generar Entrada de Changelog
Imprime los items para contexto:
python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
d = json.load(f)
print(json.dumps(d['items'], indent=2))
print()
print(f'Formato de changelog existente: {d[\"existing_changelog\"][\"format\"]}')
print(f'Última etiqueta: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'Hoy: {d[\"date\"]}')
"
Instrucciones para la IA: Transforma cada item en bruto de lenguaje técnico a un lenguaje orientado al beneficio para el usuario. Sigue references/changelog-format.md para las reglas de transformación y ejemplos.
Reglas:
- NO inventes resultados o métricas. "40% más rápido" debe provenir de los datos de origen. Si no hay número en el commit o PR, no añadas ninguno.
- Usa tiempo pasado: "Añadido", "Corregido", "Mejorado" -- no "Añade", "Corrige"
- Asigna exactamente una categoría a cada item: Nuevo, Mejorado, Corregido o Interno
- Interno: Incluir solo si es relevante para desarrolladores (cambios de API, cambios disruptivos). Omite secciones vacías.
- Omite cualquier cosa que corresponda a: cambios de pruebas, cambios de CI, commits solo de documentación
Determina la etiqueta de versión:
- Si el usuario especificó una: úsala exactamente.
- Si
existing_changelog.format == "semver": incrementa según los cambios (parche solo para correcciones, menor para cualquier característica nueva). - Por defecto:
Semana del [Mes Día, Año]usando la fecha de hoy.
Escribe la entrada en /tmp/pul-entry.json:
{
"label": "Semana del 23 de abril de 2026",
"date": "2026-04-23",
"new": [
{"title": "Modo oscuro", "description": "Actívalo en Ajustes > Apariencia. Funciona en todas las vistas."}
],
"improved": [
{"title": "Tiempo de respuesta de la API", "description": "Un 40% más rápido de media. El panel ahora carga en menos de 1 segundo."}
],
"fixed": [
{"title": "Exportación CSV", "description": "Las exportaciones ya no omiten la última fila."}
],
"under_the_hood": []
}
Verifica la entrada:
python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
e = json.load(f)
print(f'Etiqueta: {e[\"label\"]}')
total = 0
for cat in ['new', 'improved', 'fixed', 'under_the_hood']:
items = e.get(cat, [])
if items:
print(f'{cat.replace(\"_\", \" \").title()} ({len(items)}):')
for item in items:
print(f' - {item[\"title\"]}: {item[\"description\"]}')
total += len(items)
print(f'Total: {total} items')
"
Paso 5: Generar Paquete de Contenido
Usando la entrada del changelog del Paso 4, genera las cuatro piezas de contenido. Sigue references/content-rules.md estrictamente.
Frase corta (máx 20 palabras): Una oración que cubra los 1-2 items más importantes. Lenguaje sencillo, sin jerga.
Hilo de tweets (3-5 tweets):
- Tweet 1: Gancho -- "Lanzamos [N] cosas esta semana." o adelanta con la característica más importante.
- Tweets 2-N: Un item por tweet, 1-2 oraciones como máximo.
- Último tweet: "Changelog: [enlace]" o "La semana que viene más." (opcional)
- Cada tweet estrictamente con menos de 280 caracteres.
- Sin hashtags. Sin guiones largos. Voz activa.
Publicación de LinkedIn:
- Sin markdown (los asteriscos se renderizan literales en LinkedIn)
- Sin hashtags
- Voz de fundador: "Lanzamos", no "Estamos emocionados de anunciar"
- Párrafos cortos (1-2 oraciones cada uno), líneas en blanco entre ellos
- Cerrar con una pregunta u observación, no con una llamada a la acción
- 150-400 palabras en total
Fragmento de correo electrónico:
- Asunto: "Lo que lanzamos esta semana: [item más importante] + [1 más]"
- Cuerpo: 50-100 palabras. "Esto es lo que lanzamos esta semana:" luego viñetas.
Escribe en /tmp/pul-content.json:
{
"one_liner": "Modo oscuro, API más rápida y un error de exportación corregido.",
"tweet_thread": [
"Lanzamos 3 cosas esta semana.",
"El modo oscuro ya está disponible. Actívalo en Ajustes > Apariencia. Funciona en todas partes.",
"El tiempo de respuesta de la API ahora es un 40% más rápido. El panel carga en menos de un segundo.",
"Corregido: las exportaciones CSV omitían la última fila. Eso ya no ocurre.",
"Changelog: [enlace]"
],
"linkedin_post": "Lanzamos 3 actualizaciones esta semana.\n\nEl modo oscuro ya está disponible. Actívalo en Ajustes, bajo Apariencia. Funciona en todas las vistas.\n\nEl tiempo de respuesta de la API es un 40% más rápido de media. El panel ahora carga en menos de un segundo para la mayoría de los usuarios.\n\nTambién corregimos un error en el que las exportaciones CSV omitían silenciosamente la última fila. Si te pasó y dejaste de exportar, vale la pena intentarlo de nuevo.\n\n¿Qué característica has estado esperando?",
"email_snippet": {
"subject": "Lo que lanzamos esta semana: modo oscuro + API más rápida",
"body": "Esto es lo que lanzamos esta semana:\n\n- Modo oscuro: actívalo en Ajustes > Apariencia\n- Tiempo de respuesta de la API: 40% más rápido, el panel carga en menos de 1 segundo\n- Corregido: las exportaciones CSV ya no omiten la última fila\n\nChangelog completo a continuación."
}
}
Paso 6: Autoverificación
python3 -c "
import json, re
with open('/tmp/pul-raw.json') as f:
raw = json.load(f)
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
full_text = json.dumps(entry) + json.dumps(content)
fails = 0
# Verificación 1: Sin guiones largos
if chr(8212) in full_text:
print('FALLO: se encontró guión largo -- reemplázalo con guión normal')
fails += 1
else:
print('APROBADO: sin guiones largos')
# Verificación 2: Palabras prohibidas
banned = ['poderoso', 'robusto', 'sin fisuras', 'innovador', 'revolucionario',
'optimizar', 'aprovechar', 'transformar', 'revolucionar', 'emocionados de anunciar',
'contentos de anunciar', 'estamos encantados', 'de vanguardia', 'de primera clase',
'de clase mundial', 'desbloquear', 'encantador']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FALLO: palabras prohibidas encontradas: {found}')
fails += 1
else:
print('APROBADO: sin palabras prohibidas')
# Verificación 3: Longitud de tweets
thread = content.get('tweet_thread', [])
long_tweets = [(i+1, len(t)) for i, t in enumerate(thread) if len(t) > 280]
if long_tweets:
print(f'FALLO: tweets de más de 280 caracteres: {long_tweets}')
fails += 1
else:
print(f'APROBADO: todos los {len(thread)} tweets tienen menos de 280 caracteres')
# Verificación 4: LinkedIn sin hashtags
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('FALLO: hashtags encontrados en la publicación de LinkedIn')
fails += 1
else:
print('APROBADO: sin hashtags en LinkedIn')
# Verificación 5: Sin markdown en LinkedIn
if '**' in li or '__' in li:
print('FALLO: formato markdown en LinkedIn (se renderiza como asteriscos literales)')
fails += 1
else:
print('APROBADO: sin markdown en LinkedIn')
# Verificación 6: Recuento de palabras de la frase corta
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
print(f'FALLO: la frase corta tiene {word_count} palabras (máximo 20)')
fails += 1
else:
print(f'APROBADO: la frase corta tiene {word_count} palabras')
# Verificación 7: Recuento de items
entry_items = (len(entry.get('new', [])) + len(entry.get('improved', [])) +
len(entry.get('fixed', [])) + len(entry.get('under_the_hood', [])))
raw_total = raw['total_items']
print(f'INFO: {entry_items} items de changelog de {raw_total} items en bruto')
print()
print(f'Resultado: {\"APROBADO\" if fails == 0 else f\"FALLO ({fails} problema(s))\"}')
"
Si alguna verificación falla: Corrige el problema en el archivo temporal relevante antes de proceder al Paso 7. Vuelve a ejecutar la verificación después de corregir.
Paso 7: Añadir al Changelog y Guardar Contenido
python3 << 'PYEOF'
import json, os, re
with open('/tmp/pul-entry.json') as f:
entry = json.load(f)
with open('/tmp/pul-content.json') as f:
content = json.load(f)
# Construir la nueva sección del changelog
lines = [f"## {entry['label']}", ""]
CAT_HEADERS = {
"new": "### Nuevo",
"improved": "### Mejorado",
"fixed": "### Corregido",
"under_the_hood": "### Interno",
}
for cat, header in CAT_HEADERS.items():
items = entry.get(cat, [])
if items:
lines.append(header)
for item in items:
lines.append(f"- **{item['title']}** -- {item['description']}")
lines.append("")
lines.append("---")
lines.append("")
new_section = "\n".join(lines)
# Anteponer a docs/changelog.md
os.makedirs("docs", exist_ok=True)
changelog_path = "docs/changelog.md"
if os.path.exists(changelog_path):
existing = open(changelog_path).read()
# Insertar después del encabezado de nivel superior (si lo hay) o al principio
if existing.startswith("# "):
end_of_heading = existing.index("\n") + 1
updated = existing[:end_of_heading] + "\n" + new_section + existing[end_of_heading:]
else:
updated = new_section + existing
else:
updated = "# Changelog\n\n" + new_section
with open(changelog_path, "w") as f:
f.write(updated)
print(f"Changelog actualizado: {changelog_path}")
# Guardar paquete de contenido
date = entry['date']
content_dir = "docs/product-updates"
os.makedirs(content_dir, exist_ok=True)
content_path = f"{content_dir}/{date}-content.md"
content_lines = [
f"# Paquete de Contenido: {entry['label']}",
"",
"## Frase corta",
content.get('one_liner', ''),
"",
"## Hilo de tweets",
"",
]
thread = content.get('tweet_thread', [])
for i, tweet in enumerate(thread, 1):
content_lines.append(f"[{i}/{len(thread)}] {tweet}")
content_lines.append("")
content_lines += [
"## Publicación de LinkedIn",
"",
content.get('linkedin_post', ''),
"",
"## Fragmento de correo electrónico",
"",
f"Asunto: {content.get('email_snippet', {}).get('subject', '')}",
"",
content.get('email_snippet', {}).get('body', ''),
"",
]
with open(content_path, "w") as f:
f.write("\n".join(content_lines))
print(f"Paquete de contenido: {content_path}")
PYEOF
Paso 8: Limpiar y Presentar
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "Listo."
Presenta al usuario en este orden:
1. Entrada de changelog (formato markdown, no JSON en bruto):
## Semana del 23 de abril de 2026
### Nuevo
- **Modo oscuro** -- Actívalo en Ajustes > Apariencia. Funciona en todas las vistas.
### Mejorado
- **Tiempo de respuesta de la API** -- Un 40% más rápido de media. El panel ahora carga en menos de 1 segundo.
### Corregido
- **Exportación CSV** -- Las exportaciones ya no omiten la última fila.