---
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" <workspace_root>)"
```

- 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_<repo>.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:*_ <Instance Name> / Usuarios: <N>`
- Si solo hay Instance Name (sin usuarios): `_*🏢 Cliente:*_ <Instance Name>`
- Si solo hay conteo de usuarios (sin Instance Name): `_*🏢 Usuarios afectados:*_ <N>`
- `<N>` = 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 `<url|texto>` 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.
