--- name: summary description: Use when generating a daily digest of merged PRs grouped by Jira ticket — supports Slack publishing --- # /summary — Resumen diario (Humand Tech Plugin) Resumen de cambios del **día anterior** en humand-web, humand-mobile y humand-backoffice. Los PRs se vinculan a tickets JIRA; el resumen se agrupa por tribu y squad y opcionalmente se envía a Slack. ## Plugin-first adaptation Antes de los pasos (Paso 1 en adelante), resolver rutas efectivas: ```bash eval "$(bash "${CLAUDE_SKILL_DIR}/../../scripts/workflow-setup.sh" )" ``` - Usar **`$REPOS_FILE`** y **`$TEAMS_FILE`** en todos los prerequisitos y pasos de clasificación. ## Superpowers-first guard - Usar `writing-plans` para la ejecución multi-fase. - Usar `verification-before-completion` antes de afirmar que el resumen se envió a Slack. - No declarar éxito de envío sin evidencia de respuesta de API. --- ## Resumen del flujo El skill opera en **3 fases estrictamente separadas**. No mezclar fases. ### FASE 1 — FETCH (toda la red aquí) - 0. Verificar día laboral (sábado/domingo → terminar) - 1. Timeout global (registrar hora de inicio) - 2–3. Verificar entornos + prerequisitos + auth GitHub - 4. Calcular ventana de tiempo (fecha completa con año, ART UTC-3) - 5a. GitHub: fetch PRs por repo → `/tmp/prs_.json` (3 archivos) - 5b. **JIRA batch:** extraer tickets únicos de todos los PRs → fetch todos a la vez → `/tmp/jira_cache.json` (1 archivo con todos los issues + padres) ### FASE 2 — COMPOSE (sin red; leer /tmp/, escribir payloads) - 6–8. Clasificar PRs por tribu/squad (labels + teams.json) - 9. Leer `/tmp/jira_cache.json` — **no más llamadas JIRA en este paso** - 10. Sintetizar Problema/Solución/QA Manual por PR - 11. Escribir `/tmp/slack_basic.json` + `/tmp/slack_thread_N.json` - 12a. Gate de calidad (verificar anti-patterns, idioma, formato) ### FASE 3 — SEND + CLEANUP - 12b. Enviar a Slack (idempotencia, hilo, rollback si falla) - 12c. **Cleanup obligatorio:** `rm -f /tmp/prs_* /tmp/jira_cache.json /tmp/slack_*` — siempre, éxito o fallo --- ## Paso 0. Verificar día laboral Antes de cualquier otra acción, verificar el día de la semana actual: - **Lunes a viernes (1–5):** continuar con el skill normalmente. - **Sábado o domingo (6–7):** terminar inmediatamente con el mensaje: «ℹ️ Hoy es fin de semana. El resumen solo se genera para días laborales (lunes a viernes). No hay nada que hacer.» Salir con código 0 (no es un error). **No ejecutar ningún paso posterior.** --- ## Timeout global El agente debe registrar la hora de inicio al comenzar `/summary`. Si transcurren **30 minutos** sin haber completado el envío a Slack (o la presentación del resumen en pantalla), **abortar inmediatamente** y mostrar al usuario: «El resumen no pudo completarse en 30 minutos. Revisar logs y reintentar con `/summary`.» --- ## Paso 1. Verificar entornos Cargar `.env` si existe en la raíz. | Variable | Requerida | Uso | | ----------------- | --------- | ------------------------------------------------------------------- | | `GH_TOKEN` | Sí | GitHub API (gh/GraphQL). Sin él no continuar. | | `JIRA_EMAIL` | Recomendada | Email de la cuenta Atlassian para fallback REST API (si MCP falla). | | `JIRA_API_TOKEN` | Recomendada | API token de Jira (no expira). Crear en https://id.atlassian.com/manage-profile/security/api-tokens. Necesario si el MCP OAuth no está disponible. | | `SLACK_BOT_TOKEN` | No | Envío a Slack al final. Si falta, solo mostrar resumen en pantalla. | **Si falta GH_TOKEN:** no continuar. Decir: «Para ejecutar /summary necesitas `GH_TOKEN`. Añádelo en `.env` (ver `.env.example`). Vuelve a ejecutar /summary cuando esté configurado.» **Si faltan JIRA_EMAIL o JIRA_API_TOKEN:** no abortar todavía. Registrar internamente que el fallback REST API no está disponible (`JIRA_REST_AVAILABLE=false`). El skill intentará el MCP en el Paso 5b; si el MCP también falla, abortará indicando que ni MCP ni REST API están disponibles. **Si falta SLACK_BOT_TOKEN:** avisar una vez y seguir: «`SLACK_BOT_TOKEN` no está definido; el resumen se mostrará aquí pero no se enviará a Slack. Para enviar a Slack, añade el token en `.env`.» --- ## Paso 2. Prerequisitos | Requisito | Si falta | | -------------------- | -------------------------------------------------------------------------------- | | `gh` | Indicar instalar (ej. `brew install gh`) y detener. | | `jq` | Indicar instalar y detener. | | `$REPOS_FILE` | Detener; pedir configurar el repo. | | `$TEAMS_FILE` | Avisar y continuar; tickets como "Sin ticket" cuando no se resuelva squad/tribu. | --- ## Paso 3. Auth GitHub Ejecutar `gh api user -q .login` (con entorno ya cargado). Si devuelve login → OK. Si falla: recordar que el token va en `.env`. Si sigue fallando, detener y mostrar: «Autenticación con GitHub falló. Revisa que `GH_TOKEN` esté definido y sea válido en `.env`. Crea un PAT con scope `repo` y `read:user`. Vuelve a ejecutar /summary cuando esté resuelto.» **[DEBUG — imprimir siempre]:** Antes de continuar al Paso 4, mostrar en pantalla: ```bash GH_LOGIN=$(gh api user -q .login 2>/dev/null || echo "ERROR") echo "DEBUG [Paso 3] GH_TOKEN presente: ${GH_TOKEN:+SI (${#GH_TOKEN} chars)}" && echo "DEBUG [Paso 3] GH login: $GH_LOGIN" ``` Si `GH_LOGIN` es `ERROR` → abortar con el mensaje de arriba. Mostrar el debug incluso cuando la auth es exitosa. --- ## Paso 4. Ventana de tiempo **Fecha de hoy (obligatorio):** Siempre obtener la **fecha completa del día de hoy** (día, mes y **año**) antes de calcular la ventana. Usar una fuente explícita: por ejemplo `date +%Y-%m-%d` en la zona horaria del agente, o el dato de sesión que indique «hoy es DD/MM/AAAA». No asumir ni inventar el año (evitar fetches incorrectos; ej. usar 2025 cuando hoy es 2026). Filtrar PRs por `mergedAt` en hora **ART (UTC-3)**. Ventana = día anterior 00:00 ART → hoy 00:00 ART (fin exclusivo). - **Lunes:** el «día anterior» abarca viernes, sábado y domingo. Ventana = viernes 00:00 ART → lunes 00:00 ART. Se deben incluir en el resumen los PRs mergeados el viernes, sábado y domingo. - **Martes a viernes:** un solo día (ayer 00:00 ART → hoy 00:00 ART). - **Sábado/domingo:** no generar resumen; avisar que solo se hace para días laborales. En UTC: día anterior 03:00 UTC → hoy 03:00 UTC (ej. hoy 12/03/2026 → `2026-03-11T03:00:00Z` ≤ `mergedAt` < `2026-03-12T03:00:00Z`). **[DEBUG — imprimir siempre]:** Inmediatamente después de calcular la ventana, ejecutar y mostrar en pantalla: ```bash TODAY_SHELL=$(date +%Y-%m-%d) DOW_SHELL=$(date +%u) # 1=lun … 7=dom echo "DEBUG [Paso 4] date shell: TODAY=$TODAY_SHELL DOW=$DOW_SHELL" echo "DEBUG [Paso 4] ventana calculada: SINCE=$SINCE UNTIL=$UNTIL" echo "DEBUG [Paso 4] año en SINCE: $(echo $SINCE | cut -c1-4) (debe ser >= 2026)" ``` Si el año extraído de `$SINCE` es < 2026 → **abortar inmediatamente** con: «ERROR [DEBUG] Año calculado incorrecto: $SINCE — el agente usó una fecha inventada. Revisar Paso 4 y volver a ejecutar.» --- ## Paso 5. Obtener PRs mergeados (siempre por repo) **Regla:** hacer **un request por repositorio**. No concatenar los 3 repos en una sola query ni parsear varias respuestas en un solo blob. 1. Repos: `humand-web`, `humand-backoffice`, `humand-mobile` (owner `HumandDev`). 2. Por cada repo, usar **exactamente** este comando (no reformular la query): ```bash GQL_QUERY='query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(first:100,states:[MERGED],baseRefName:"develop",orderBy:{field:UPDATED_AT,direction:DESC}){nodes{number title mergedAt url body labels(first:20){nodes{name}}}}}}' RAW=$(gh api graphql -f query="$GQL_QUERY" -f owner=HumandDev -f name="$REPO") ``` Puntos críticos: `states:[MERGED]` con corchetes de array (sin ellos GitHub devuelve error de schema). **No añadir `2>/dev/null`** — los errores de GraphQL deben ser visibles. 3. La API de GitHub **no** filtra por fecha en esta query. El filtro se hace **después** con `jq`: ```bash FILTERED=$(echo "$RAW" | jq --arg s "$SINCE" --arg u "$UNTIL" \ '[.data.repository.pullRequests.nodes[]? | select(.mergedAt != null and .mergedAt >= $s and .mergedAt < $u)]') ``` Ejemplo: `SINCE="2026-03-11T03:00:00Z"` y `UNTIL="2026-03-12T03:00:00Z"` para el 11/03 en ART. Guardar los PRs filtrados en `/tmp/prs_${REPO}.json` para usarlos en pasos siguientes. **[DEBUG — imprimir siempre]:** Por cada repo, después del fetch y del filtro jq, mostrar: ```bash # RAW = total de nodos devueltos por la API (sin filtrar por fecha) # FILTERED = PRs que caen dentro de la ventana SINCE..UNTIL echo "DEBUG [Paso 5] $REPO — raw nodes: $RAW_COUNT filtrados por ventana: $FILTERED_COUNT" # Si hay nodos raw pero filtrados=0, mostrar el mergedAt del primer nodo para detectar desfase de fechas: if [ "$RAW_COUNT" -gt 0 ] && [ "$FILTERED_COUNT" -eq 0 ]; then echo "DEBUG [Paso 5] $REPO — mergedAt del 1er nodo: $FIRST_MERGED_AT (ventana esperada: $SINCE .. $UNTIL)" fi ``` Si `RAW_COUNT` > 0 y `FILTERED_COUNT` == 0 en todos los repos → el problema es la ventana de fechas (SINCE/UNTIL incorrectos). Mostrar explícitamente: «DEBUG: fetch OK pero ventana incorrecta — revisar Paso 4.» --- ## Paso 5b. Batch JIRA fetch → /tmp/jira_cache.json **Este paso cierra la Fase 1.** Toda la red JIRA ocurre aquí, en un único loop, antes de la composición. Al terminar este paso el agente tiene todos los datos necesarios en `/tmp/` y no debe hacer más llamadas de red hasta el envío a Slack. ### Proveedor Intentar **MCP `user-atlassian`** primero (Nivel 1 del Paso 9 anterior). Si falla (auth, timeout, "Aborted"), usar **REST API** (`curl` + `JIRA_EMAIL`/`JIRA_API_TOKEN`). La elección se hace una vez al inicio del batch — no alternar por ticket. ### Script de referencia (REST API) ```bash # 1. Extraer todos los ticket keys únicos de los 3 repos TICKETS=$(python3 - << 'PY' import sys, json, re files = [ "/tmp/prs_humand-web.json", "/tmp/prs_humand-backoffice.json", "/tmp/prs_humand-mobile.json", ] ticket_re = re.compile(r'\[([A-Z]{2,10}-\d+)\]') keys = set() for path in files: try: prs = json.load(open(path)) for pr in prs: text = (pr.get("title") or "") + " " + (pr.get("body") or "") for m in ticket_re.finditer(text): keys.add(m.group(1)) except Exception: pass print("\n".join(sorted(keys))) PY ) echo "DEBUG [Paso 5b] tickets a fetchear: $(echo "$TICKETS" | wc -w | tr -d ' ')" # 2. Inicializar cache vacío echo "{}" > /tmp/jira_cache.json # 3. Fetch de cada ticket for KEY in $TICKETS; do RESP=$(curl -s -w "\n%{http_code}" \ -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \ -H "Accept: application/json" \ "https://humand.atlassian.net/rest/api/3/issue/$KEY?fields=summary,issuetype,description,status,assignee,parent,project") HTTP_CODE=$(echo "$RESP" | tail -1) ISSUE_BODY=$(echo "$RESP" | sed '$d') if [ "$HTTP_CODE" != "200" ]; then echo "DEBUG [Paso 5b] $KEY → HTTP $HTTP_CODE — se usará '—' en Problema/Solución/QA" continue fi # Detectar si necesita card padre IS_SUBTASK=$(echo "$ISSUE_BODY" | jq -r '.fields.issuetype.subtask // false') DESC_TEXT=$(echo "$ISSUE_BODY" | jq -r '[.fields.description | recurse(.content[]?) | .text // empty] | join(" ")' 2>/dev/null || echo "") PARENT_KEY=$(echo "$ISSUE_BODY" | jq -r '.fields.parent.key // empty') NEEDS_PARENT="false" if [ "$IS_SUBTASK" = "true" ] || [ -z "$DESC_TEXT" ] || echo "$DESC_TEXT" | grep -qE "Subtask Summary|Use as you need"; then [ -n "$PARENT_KEY" ] && NEEDS_PARENT="true" fi if [ "$NEEDS_PARENT" = "true" ]; then PARENT_RESP=$(curl -s -w "\n%{http_code}" \ -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \ -H "Accept: application/json" \ "https://humand.atlassian.net/rest/api/3/issue/$PARENT_KEY?fields=summary,issuetype,description,status,assignee,parent,project") PARENT_HTTP=$(echo "$PARENT_RESP" | tail -1) PARENT_BODY=$(echo "$PARENT_RESP" | sed '$d') if [ "$PARENT_HTTP" != "200" ]; then echo "DEBUG [Paso 5b] $KEY → parent $PARENT_KEY HTTP $PARENT_HTTP — usando solo issue" ENTRY=$(jq -n --argjson issue "$ISSUE_BODY" '{"issue": $issue, "parent": null}') else ENTRY=$(jq -n --argjson issue "$ISSUE_BODY" --argjson parent "$PARENT_BODY" \ '{"issue": $issue, "parent": $parent}') echo "DEBUG [Paso 5b] $KEY → OK (subtask, parent=$PARENT_KEY fetched)" fi else ENTRY=$(jq -n --argjson issue "$ISSUE_BODY" '{"issue": $issue, "parent": null}') echo "DEBUG [Paso 5b] $KEY → OK" fi # Acumular en cache jq --arg k "$KEY" --argjson v "$ENTRY" '. + {($k): $v}' \ /tmp/jira_cache.json > /tmp/jira_cache_tmp.json \ && mv /tmp/jira_cache_tmp.json /tmp/jira_cache.json done CACHED=$(jq 'keys | length' /tmp/jira_cache.json) echo "DEBUG [Paso 5b] jira_cache.json listo: $CACHED tickets cacheados" ``` ### Estructura de `/tmp/jira_cache.json` ```json { "SQSH-3607": { "issue": { ...campos Jira del issue... }, "parent": { ...campos Jira del padre, o null... } }, "SQZB-5157": { ... } } ``` Si un ticket no pudo fetchearse (HTTP != 200), **no aparece en el cache**. El Paso 9 detecta la ausencia y usa `"—"` para los 3 campos de ese PR. --- ## Paso 6. Squads y labels (referencia) - **Squads:** leer de `$TEAMS_FILE`. Cada squad tiene `name` y opcionalmente `emoji` (animal que representa al squad). En el mensaje del resumen mostrar **emoji + nombre** (ej. 🐆 Jaguar, 🦓 Zebra). Las tribus (`_tribes`) agrupan squads. - **Labels (ignorar):** don't merge, 🚧 On Hold, Release. - **Labels tipo fix (para tag en título del PR):** si el PR tiene `stg fix` o `🧪 Bugfix` → añadir ` · [STG]` en la línea del título; si tiene `hot fix` o `🚨 Hotfix` → añadir ` · [HOTFIX]`. Solo si el PR tiene ese label. - **Labels tribu:** comm/Communication, ops/Operations, talent/Talent & Data, time management/Time & People, people foundation, data, Tech → usar para agrupar; si no hay label de tribu → No tribe dentro de No ticket. --- ## Paso 7. Labels de los PRs Los PRs pueden tener **labels** que aportan dos tipos de información. **Ignorar** para clasificación: "don't merge" / "🚧 don't merge", "🚧 On Hold", "Release" (solo indican estado o bloqueo). ### A) Tipo de fix (además de develop) Indican si el PR, además de mergearse a develop, **también se subió** a otro entorno. Reconocer en **todos los repos** (humand-web y humand-backoffice usan un nombre; humand-mobile puede usar otro): | Label (web / backoffice) | Label (humand-mobile) | Significado | | ------------------------ | --------------------- | ---------------- | | **stg fix** | **🧪 Bugfix** | Fix a Staging | | **hot fix** | **🚨 Hotfix** | Fix a Producción | Si el PR no tiene ninguna de estas labels, solo se considera merge a develop. ### B) Tribu Indican **la tribu a la que pertenece** el desarrollo. Usar para agrupar tanto PRs con ticket como PRs sin ticket (sección "No ticket"). Mapeo unificado **label → tribu** (normalizar por nombre de tribu para que web, backoffice y mobile queden alineados): | Label (web / backoffice) | Label (humand-mobile) | Tribu (nombre canónico) | | ------------------------ | ----------------------------- | ----------------------- | | **comm** | **Communication** | Communication | | **data** | _(usar Data o Talent & Data)_ | Data | | **ops** | **Operations** | Operations | | **people foundation** | _(no suele usarse en mobile)_ | People Foundation | | **talent** | **Talent & Data** | Talent | | **time management** | **Time & People** | Time Management | | _(cross / tech)_ | **Tech** | Tech / Cross | Al clasificar, si el PR tiene alguna label de tribu (en cualquiera de sus variantes por repo), asignar la tribu canónica. Si no tiene ninguna label de tribu, va a **No tribe** dentro de "No ticket". --- ## Paso 8. Vincular con JIRA y clasificar **Omitir** PRs cuyo título contenga `[Hot fix]`, `[STG FIX]` (o variantes) o `Backport`. No listarlos. **Ticket:** buscar en título `[SQXX-NNNN]` o `[CSBM-NNNN]` (prefijos en teams.json). Si no en título, buscar en body. Si aparece `[NO-CARD]` o no se encuentra clave → sin ticket. **Con ticket:** agrupar por ticket/squad (Paso 6). **Sin ticket:** sección **No ticket**, subdividida por tribu (según labels del Paso 7) o **No tribe** si no tiene label de tribu. --- ## Paso 9. Leer contexto JIRA del cache (sin red) **Este paso pertenece a la Fase 2 (Compose). No hacer ninguna llamada de red aquí.** Todos los datos JIRA ya están en `/tmp/jira_cache.json` gracias al Paso 5b. ### Leer por ticket Para cada PR con ticket, buscar la clave en el cache: ```python import json cache = json.load(open("/tmp/jira_cache.json")) entry = cache.get(ticket_key) # ej. cache.get("SQSH-3607") ``` | Caso | Qué usar | |------|----------| | `entry` existe y `entry["parent"]` no es `null` | Usar `entry["parent"]` como fuente principal de descripción/pasos (card padre). Complementar con `entry["issue"]` si el padre también tiene poco contenido. | | `entry` existe y `entry["parent"]` es `null` | Usar `entry["issue"]` directamente. | | `entry` no existe (fetch falló en Paso 5b) | Escribir `"—"` en Problema, Solución y QA Manual. No inventar contenido. | ### Extraer texto de la descripción (ADF) El campo `.fields.description` viene en Atlassian Document Format. Extraer texto plano: ```bash echo "$ISSUE_JSON" | jq -r '[.fields.description | recurse(.content[]?) | .text // empty] | join(" ")' ``` Si `.fields.description` es `null` → resultado vacío → considerar como sin descripción útil. ### Extraer Instance Name y usuarios afectados Del texto de descripción de la card elegida (issue o padre): - **Instance Name:** buscar el patrón `Instance Name:` o `**Instance Name**:` seguido del valor concreto (una palabra o nombre corto). Si el valor es `-` o está ausente → ignorar. - **Usuarios afectados:** buscar la sección `Usuario/s Afectado/s`; contar bullets que **no** sean `-`. Si todos son `-` o el campo está ausente → conteo = 0. **NUNCA copiar el texto completo de la descripción Jira en el campo Cliente.** Solo extraer los dos valores puntuales. Si la descripción es el template con todos los campos en `-` → omitir completamente la línea Cliente. --- ## Paso 10. Generar Problema, Solución y QA Manual **Problema**, **Solución** y **QA Manual** deben generarse a partir de estas tres fuentes (no inventar ni usar solo el título del PR): 1. **Contexto JIRA (obligatorio para PRs con ticket):** descripción de la card del ticket o de la **card padre** si la card está vacía o es sub-card; pasos para reproducir, imágenes/videos, comportamiento esperado (obtenidos en el Paso 9). 2. **Información del PR:** body del PR y comentarios (qué se hizo, por qué). 3. **Contexto del código modificado:** archivos y cambios del PR para resumir el problema que se resolvió y cómo. Con esas tres fuentes el **agente** redacta los textos (máx. 140 caracteres cada uno) para Problema, Solución y QA Manual. Es decir: el agente hace el resumen leyendo JIRA (siempre que haya ticket), body/comentarios del PR y cambios de código; no se delega a un script ni se deja en blanco. **PROHIBIDO — nunca escribir estos patrones (el gate del Paso 12 los rechazará):** - Problema que empiece con `"Contexto: "` seguido del título del PR. - Solución que empiece con `"Cambio entregado:"` o que sea solo el título del PR. - QA Manual genérico como: `"Probar el flujo del módulo en develop y regresión básica"`, `"Reproducir pasos de Jira y validar el resultado esperado en develop"`, `"Manual test on staging/production after deploy"`, o cualquier variante de estas frases. Si Jira no tiene descripción útil **y** el PR body está vacío: escribir `"—"` en el campo afectado. **Nunca inventar texto genérico** como fallback. **Idioma (obligatorio para Slack):** Problema, Solución y QA Manual deben estar redactados en **español**, aunque Jira, el body del PR o los comentarios estén en inglés. El agente **traduce y sintetiza**; no copia párrafos en inglés ni deja etiquetas mezcladas (español en cabeceras, inglés en el cuerpo). **No copiar la plantilla del PR:** Los encabezados del template de GitHub (`## Summary`, `### What Changed`, `### Test Plan`, etc.) **no** son contenido válido para Solución ni Problema. Si el body del PR está vacío o solo tiene esas secciones sin bullets útiles, basarse en **Jira + diff** para redactar los tres campos (ver también validación pre-envío en Paso 12, Fase A). **Regla para Cliente / Usuarios Afectados:** incluir la línea `🏢 Cliente` justo debajo de Problema, **solo si** al menos uno de los dos valores está disponible en la card JIRA (o card padre): - Formato: `_*🏢 Cliente:*_ / Usuarios: ` - Si solo hay Instance Name (sin usuarios): `_*🏢 Cliente:*_ ` - Si solo hay conteo de usuarios (sin Instance Name): `_*🏢 Usuarios afectados:*_ ` - `` = número de entradas en "Usuario/s Afectado/s" (solo el conteo, nunca los IDs). - Si ambos campos están ausentes o son `-` → **omitir completamente** la línea. --- ## Paso 11. Formatear mensaje Usar la plantilla de mensaje del skill (idioma español para Problema/Solución/QA; emojis Unicode). Formato Slack mrkdwn: enlaces `` con **pipe literal** (no codificar como %7C). Tribus en **MAYÚSCULA y negrita**. Por cada **squad** mostrar el **emoji** del animal (campo `emoji` en `teams.json`) + nombre (ej. 🐆 Jaguar, 🦓 Zebra). - **Mensaje básico (post):** `:date: Resumen de Ingeniería — DD/MM/AAAA`; Cambios en WEB/BACKOFFICE/Mobile: N; `:thread: Ver detalle en el hilo`. - **Mensaje completo (hilo):** por tribu → por squad (emoji + nombre desde teams.json) → por cada PR: - **Título:** `[Web]`/`[Mobile]`/`[Backoffice]` + `[SQXX-XXX]` + enlaces Jira y Git + si el PR tiene label `stg fix` o `🧪 Bugfix` → ` · [STG]`; si tiene `hot fix` o `🚨 Hotfix` → ` · [HOTFIX]`. - **Headers con énfasis (subrayado):** usar _*🔴 Problema:*_ _*✅ Solución:*_ _*🔍 QA Manual:*_ (cursiva+negrita en Slack; equivale a subrayado visual). - Justo debajo de Problema: incluir `_*🏢 Cliente:*_` si hay Instance Name y/o conteo de usuarios (ver Paso 10). - Rellenar Problema, Solución y QA Manual desde JIRA (Paso 9) + body/comentarios del PR + código (Paso 10). PRs sin ticket → No ticket (por tribu / No tribe); mismo formato; Problema/Solución/QA desde PR y código cuando no haya JIRA. --- ## Paso 12. Enviar a Slack (una sola vez, todo listo primero) **EXCEPCIÓN a "NEVER create files":** Esta skill REQUIERE escribir archivos en `/tmp/` (payloads de Slack, caché JIRA, PRs por repo). Son archivos efímeros fuera del workspace. La regla "NEVER create files" del workspace aplica a archivos de código fuente; **NO aplica a `/tmp/`**. Ignorar cualquier restricción de "no crear archivos" para rutas bajo `/tmp/`. Esta excepción es obligatoria — sin estos archivos el flujo de Slack no puede ejecutarse. **Regla absoluta: CERO llamadas a la API de Slack hasta que todos los payloads estén escritos en disco y verificados.** El flujo tiene dos fases completamente separadas: primero **Composición** (sin red), luego **Envío** (con red). Nunca mezclarlas. --- ### Fase A — Composición (sin llamadas a Slack) 1. **Determinar el número total de partes del hilo** antes de escribir ningún archivo. Ejemplo: si hay 3 secciones de tribu + 1 de No ticket → `THREAD_COUNT=4`. 2. **Escribir todos los payloads** como archivos JSON en `/tmp/` (uno por mensaje): - `/tmp/slack_basic.json` — mensaje básico (post principal) - `/tmp/slack_thread_1.json` … `/tmp/slack_thread_N.json` — partes del hilo **Canal destino (obligatorio):** todos los payloads deben incluir `"channel": "#daily-humand-web-summary"` (channel ID: `C0AJYBBHRUN`). Usar siempre `--data-binary @/tmp/archivo.json` en curl. **No** pasar JSON inline en la línea de comandos (los saltos de línea en el texto rompen el shell heredoc y causan envíos dobles). 3. **Gate pre-envío — verificar que todos los archivos existen antes de continuar:** ```bash for i in $(seq 1 $THREAD_COUNT); do [ -f "/tmp/slack_thread_${i}.json" ] || { echo "FALTA /tmp/slack_thread_${i}.json — abortar"; exit 1; } done [ -f "/tmp/slack_basic.json" ] || { echo "FALTA /tmp/slack_basic.json — abortar"; exit 1; } echo "Gate OK: todos los payloads listos" ``` **Si algún archivo falta → abortar completamente, sin enviar nada.** No es aceptable enviar el mensaje básico y dejar el hilo incompleto. 4. **Gate de calidad del contenido (obligatorio, antes de Fase B):** Leer el texto embebido en `/tmp/slack_basic.json` y en cada `/tmp/slack_thread_*.json`. **No** pasar a Fase B hasta cumplir todo lo siguiente; si falla, **regenerar** los bloques afectados (Paso 10) y reescribir los JSON, sin llamar a Slack. | Comprobar | Acción si falla | |-----------|-----------------| | Ningún payload contiene la cadena literal `## Summary` (ni otros títulos de plantilla de PR como único “contenido” de Solución o Problema) | Volver a sintetizar Solución/Problema desde Jira + PR + código; no extraer texto crudo del body si solo quedan encabezados vacíos. | | QA Manual **no** es solo texto genérico en inglés (p. ej. `Manual test on staging/production after deploy`, variantes de “verify on staging”) | Sustituir por pasos concretos en **español** inferidos de Jira, PR y cambios. | | No aparecen códigos cortos de emoji tipo `:white_check_mark:`, `:red_circle:` en el cuerpo del mensaje | Usar caracteres Unicode en las cabeceras: `🔴` `✅` `🔍` como en MESSAGE_TEMPLATE (`_*✅ Solución:*_`, etc.). | | Problema, Solución y QA (por PR) están en **español** | Reformular desde las fuentes; ver Paso 10, regla de idioma. | | Ningún campo Problema empieza con `"Contexto: "` | Re-sintetizar desde Jira + diff; ese prefijo indica que se copió el título del PR. | | Ningún campo Solución empieza con `"Cambio entregado:"` ni es literalmente el título del PR | Re-sintetizar; ese prefijo indica que no se leyó Jira ni el código. | | QA Manual no contiene las frases `"Probar el flujo del módulo"` ni `"Reproducir pasos de Jira y validar el resultado esperado"` | Sustituir con pasos concretos inferidos de Jira, body del PR y diff. | | El campo Cliente (si aparece) no supera 200 caracteres ni contiene bloques de template Jira (ej. `Instance ID:`, `Log ID:`, `Evidence:`) | Extraer solo Instance Name y conteo numérico de usuarios; reescribir o eliminar la línea. | Este gate evita envíos rotos (plantilla copiada, inglés por defecto, mrkdwn incorrecto). --- ### Fase B — Envío (solo después del Gate OK) 5. **Idempotencia anti-doble-mensaje:** usar la fecha del resumen como clave. La variable `SUMMARY_DATE` debe ser la fecha del día resumido (formato `YYYYMMDD`). - **Archivo de idempotencia (nunca en el workspace):** definir `SUMMARY_TS_FILE="$STATE_HOME/summary-ts-${SUMMARY_DATE}.txt"`. `$STATE_HOME` viene de `workflow-setup.sh`. Antes de la primera escritura: `mkdir -p "$STATE_HOME"`. - Antes de enviar el post básico, verificar si `$SUMMARY_TS_FILE` ya existe: - **Si existe:** el post básico ya fue enviado en una ejecución anterior del mismo día. **No volver a enviarlo.** Leer el `ts` desde ese archivo y usarlo directamente para el hilo. - **Si no existe:** continuar con el envío normal. - Esto garantiza que **bajo ninguna circunstancia** aparezca un mensaje básico sin hilo, ni dos mensajes básicos el mismo día. - **Por qué fuera del repo y no `/tmp/`:** `/tmp/` se borra al reiniciar o entre sesiones, anulando el guard. El directorio de estado del usuario persiste entre reinicios sin crear archivos bajo el workspace. 6. **Enviar el mensaje básico** y capturar el `ts` (timestamp) de la respuesta: ```bash curl -s -X POST https://slack.com/api/chat.postMessage \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ --data-binary @/tmp/slack_basic.json \ -o /tmp/slack_basic_resp.json OK=$(python3 -c "import re; t=open('/tmp/slack_basic_resp.json').read(); m=re.search(r'\"ok\":(true|false)', t); print(m.group(1) if m else 'unknown')" 2>/dev/null || echo "unknown") TS=$(python3 -c "import re; t=open('/tmp/slack_basic_resp.json').read(); m=re.search(r'\"ts\":\"([^\"]+)\"', t); print(m.group(1) if m else '')" 2>/dev/null || echo "") ``` - **Guardar siempre la respuesta a un archivo** (`-o /tmp/slack_basic_resp.json`), nunca en una variable bash. Esto permite releer la respuesta sin hacer una segunda llamada a la API. - **Usar regex** (`re.search`) para extraer `ok` y `ts` del texto crudo — no `json.load`. La API de Slack devuelve el campo `text` con saltos de línea literales sin escapar (`0x0A`) dentro del JSON, lo que hace que tanto `jq` como `json.load` fallen con "Invalid string: control characters". Regex extrae los campos necesarios sin parsear el JSON completo. - **NUNCA hacer un segundo `curl -X POST` para diagnosticar un fallo** — eso envía el mensaje de nuevo. Si algo falla, leer `/tmp/slack_basic_resp.json` para ver el error sin reintentar. - Si `OK != "true"` → abortar y mostrar el contenido de `/tmp/slack_basic_resp.json` al usuario. **No reintentar.** - Si `OK == "true"` → **inmediatamente** escribir `$TS` en `$SUMMARY_TS_FILE` (clave de idempotencia), guardar `$BASIC_TS=$TS` y continuar con el hilo. 7. **Enviar cada parte del hilo** en secuencia, usando `thread_ts: $TS`: - Actualizar cada `/tmp/slack_thread_N.json` con `thread_ts` antes de enviar, o incluirlo desde el principio. - Guardar cada respuesta en `/tmp/slack_thread_N_resp.json` (mismo patrón que el mensaje básico: `-o archivo`, luego extraer `ok` con regex). - Verificar `ok == true` con regex en cada respuesta. Si alguna falla: a. **Hacer rollback:** llamar a `chat.delete` con el `ts` del mensaje básico para eliminarlo del canal y evitar un post huérfano: ```bash curl -s -X POST https://slack.com/api/chat.delete \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"channel\":\"C0AJYBBHRUN\",\"ts\":\"$BASIC_TS\"}" ``` b. **Eliminar el archivo de idempotencia** `$SUMMARY_TS_FILE` para que el próximo reintento pueda enviar el post básico nuevamente desde cero. c. Mostrar el error al usuario indicando qué parte del hilo falló y que el mensaje principal fue eliminado. d. **No reintentar automáticamente** (para evitar duplicados). 8. **No reintentar si la respuesta es ambigua.** Si curl termina con error de red o el regex no puede extraer `ok` del archivo de respuesta, aplicar el rollback del punto 7a+7b y reportar al usuario mostrando el contenido del archivo de respuesta — **sin volver a llamar a la API**. **Envío a Slack:** usar siempre el token del **bot** (`SLACK_BOT_TOKEN`), no el usuario personal, para que el resumen figure publicado por la app/bot. --- ### Fase C — Cleanup (siempre, éxito o fallo) **Ejecutar siempre al finalizar el Paso 12**, sin importar si el envío fue exitoso, hubo rollback o se abortó por gate de calidad. El objetivo es no dejar estado residual entre ejecuciones. ```bash rm -f /tmp/prs_humand-web.json \ /tmp/prs_humand-backoffice.json \ /tmp/prs_humand-mobile.json \ /tmp/jira_cache.json \ /tmp/jira_cache_tmp.json \ /tmp/slack_basic.json \ /tmp/slack_basic_resp.json \ /tmp/slack_thread_*.json \ /tmp/slack_thread_*_resp.json echo "Limpieza /tmp/ completada" ``` El archivo de idempotencia (`$STATE_HOME/summary-ts-*.txt`) **no** se borra aquí — ese persiste a propósito entre reinicios para el guard anti-doble-mensaje. --- ## Red y permisos La skill usa `gh`, MCP `user-atlassian` o `curl` (Jira REST API fallback), y opcionalmente Slack; necesita red. Para no aprobar en cada ejecución: añadir `gh` y `curl` a la allowlist de Claude Code. - **GitHub:** autenticado via `GH_TOKEN` (env var). - **JIRA (primario):** MCP `user-atlassian` (OAuth). Funciona en sesiones interactivas donde el usuario puede re-autenticar si el token expiró. - **JIRA (fallback):** `curl` + basic auth via `JIRA_EMAIL` + `JIRA_API_TOKEN` (tokens no expiran). Se activa automáticamente si el MCP falla. - **Slack:** autenticado via `SLACK_BOT_TOKEN` (bot token). Usar siempre el token del **bot** para que el resumen aparezca publicado por la app/bot, no por la cuenta del usuario.