npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thebattlearena/corner

v0.8.0

Published

Corner: tu esquina de Battle Arena en Claude Code (el plugin `battlearena`). Hooks de tracking, objetivo del turno, plan de batalla /battlearena:conquer, Ringside (el panel de tu día), el ring y la radio. Zero-dependency.

Readme

Corner: tu esquina de Battle Arena en Claude Code

En el box, la esquina es lo que asiste al peleador entre rounds. Corner es eso dentro de Claude Code: manda tus sesiones al colector (apps/public-api), nombra el objetivo de cada turno, arma tu plan de batalla, te pone la radio y la forma de trabajar del ring. Todo lo ves en Ringside, el panel de tu día junto al chat: la primera fila desde donde se ve todo mientras pasa.

Corner instala dos plugins de Claude Code: battlearena (hooks, skills y Ringside) y ring (el árbitro, el luchador y los esquineros). Las skills llevan el espacio de nombres del plugin (/battlearena:conquer), así no chocan con otros plugins y salen juntas al escribir /battlearena.

Guía rápida

1. Instalación

Necesitas Claude Code (el comando claude en el PATH) y node 18 o más reciente. Genera tu llave en battlearena.dev/api-keys y corre:

npx @thebattlearena/corner@latest install \
  --url https://api.battlearena.dev --api-key <tu-llave>

Al terminar dice qué instaló. Si ya tenías Claude Code abierto, corre /reload-plugins en cada sesión. Para actualizar, vuelve a correr npx @thebattlearena/corner@latest install: reusa tu url y tu llave, así que no hay que pasarlas otra vez.

Para quitarlo: npx @thebattlearena/corner@latest uninstall. Tu config.json se queda; bórralo si ya no vas a usar Battle Arena.

2. Si tenías la versión anterior (battle-arena-hooks)

Corner es el mismo paquete con nombre nuevo: antes se llamaba @thebattlearena/battle-arena-hooks. Para pasarte, corre:

npx @thebattlearena/corner@latest migrate

migrate es lo mismo que install: usa la misma carpeta (~/.claude/battle-arena/), así que conserva tu url, tu api-key, la radio y el tema si los tenías. De ~/.claude/settings.json solo quita los hooks que dejó la versión anterior (Corner los trae en el plugin y, si no, cada evento se mandaría dos veces); tus hooks propios no se tocan. Al terminar dice «Migrado de … a …».

  • El comando viejo npx @thebattlearena/battle-arena-hooks install también funciona: avisa del nombre nuevo y corre el instalador de Corner.
  • Si alguna vez lo instalaste global (npm install -g), el instalador te lo dice con el comando para quitarlo: npm uninstall -g @thebattlearena/battle-arena-hooks.
  • /scorecard sigue abriendo el panel, con el aviso «ahora se llama /ringside».
  • Los combates abiertos del ring siguen igual: Corner no cambia rutas ni el nombre de los plugins.

3. Arranque

  1. Abre Claude Code en tu repo. Al iniciar, el hook te saluda con tu ranking y arriba del prompt sale la banda de Ringside (Ringside LER-640 ✓ 1 ↻ 1 ↷ 1 ⚠ 1 Abrir).
  2. Escribe /ringside (o «Abrir» en la banda) para ver el panel completo. Con × escondes la banda; vuelve al abrir el panel.
  3. Si aún no tienes plan, Ringside te dice el siguiente paso: /battlearena:conquer arma el de hoy (antes de las 16:00) o el del siguiente día hábil.
  4. Trabaja normal. El objetivo de cada turno lo registra el hook; cuando cambias de tema, /battlearena:arena-objective nombra uno nuevo (Claude lo invoca solo).

4. Música (Arena FM)

La rocola de la Oficina dentro de Ringside. Se instala aparte porque necesita ffplay (de ffmpeg: brew install ffmpeg) y baja una dependencia solo en su carpeta:

npx @thebattlearena/corner@latest install --with radio

Bajo las pestañas sale ♪ título · artista con ▶/⏸, y la pestaña Radio trae la cola con sus votos y quién escucha. ▶ y ⏸ solo encienden o apagan el reproductor de tu máquina: no pausan la rocola para nadie. Votar y pedir canciones sigue en la web. El detalle, en «La radio (Arena FM) en Ringside».

5. Cómo se mide tu trabajo diario

  • El plan es la vara. Lo armas con /battlearena:conquer: puntos con ticket, DoD y tiempo comprometido (≤ 3 h cada uno). El día del plan corta a las 16:00 (CDMX).
  • Cada turno cae en un objetivo, y el objetivo se liga a un punto del plan. Si el colector duda, Claude te pregunta una vez «¿es parte del punto N o es emergente?».
  • El tiempo de un punto es la unión de sus fuentes: tus sesiones de Claude (en tramos de 5 min), las reuniones ligadas (📅), las cabinas (💡) y las sesiones de Tape (🧪). Lo simultáneo cuenta una vez.
  • El semáforo compara ese tiempo contra lo comprometido: verde, ámbar o rojo.
  • Lo que no es del plan es emergente: sale con su hora y, si desplazó un punto, te pide el motivo (✎ Responder).
  • El pie del día suma Plan · Reuniones · Fuera · Total. La pestaña Semana muestra tus hábitos.
  • Desde las 16:00 el panel es el resultado del día que cerró (modo cierre), hasta las 9:00 del siguiente día hábil o hasta que guardes el plan de ese día.

6. Conectar baterías y tareas largas por proyecto

Cada repo declara sus propias fichas en su .claude/ring.json (/ring:weigh-in te ayuda a escribirlo). Una batería de pruebas, por ejemplo:

{
  "monitors": [
    {
      "id": "bateria",
      "label": "Batería",
      "icon": "bateria",
      "color": "green",
      "match": { "command": "campaign-mock-report/scripts/run\\.mjs" }
    }
  ]
}
  • Con match.command, la ficha se enciende sola mientras Claude corre un comando que cumple la regex.
  • Un proceso que va en segundo plano, o envuelto en otro comando, avisa su estado él mismo: node "$RING/scripts/live.mjs" emit --monitor bateria --state probando --progress 1/6.
  • Cada paso puede decir si fue bien (--health ok|warn|fail --reason "…"). La ficha muestra el peor de la corrida en curso.

El detalle (esquema, íconos y colores válidos) está en «Monitores del repo».

7. Qué significa cada ícono

Con la fuente Battle Arena Icons instalada (la pone install), estos símbolos se dibujan con sus glifos; sin ella, con el carácter de la tabla.

| Ícono | Dónde | Significa | | --- | --- | --- | | ✓ | punto, banda | entregado | | ↻ | punto, banda | en curso: tiene tiempo hoy | | ↷ | punto, banda | desplazado por un emergente | | ○ | punto, banda | por hacer: sin tiempo todavía | | ● | punto, en el cierre | avanzó pero no se entregó | | ⚠ | banda, riesgos | riesgo abierto; en una ficha, «va con avisos» | | ✗ | ficha | un paso falló | | ✎ | emergente, banda | falta responder el motivo | | ✋ | riesgo | levantar la mano: avisa a tu líder | | 👍 👎 | riesgo | si el aviso de riesgo acertó o no (ajusta la detección) | | 📅 | punto | reunión ligada | | 💡 | punto, complemento | tiempo en cabina | | 🧪 | punto, complemento | sesión de Tape | | ▸ ▾ ▴ | filas | desplegar, contraer, cerrar el detalle | | ↗ | ligas | abrir en la web | | ♪ ▶ ⏸ ▲ | radio | la canción, escuchar, dejar de escuchar, votar | | spinner | ring | alguien del combate está trabajando | | ■ (ámbar) | ring | te toca decidir | | 👊N (rojo) | ring | Copilot dejó N hilos | | ◣ | ficha de Copilot | en la esquina: Copilot está revisando |

Las fichas del ring son el árbitro, el luchador, el esquinero, la campana (round-bell), Copilot y el juez (tú). Su borde dice el estado: gris quieta, azul trabajando, morada en la esquina, roja golpeó, ámbar te toca.

Lo que trae Corner

Skills

Se invocan escribiendo el comando o pidiéndolo con tus palabras («arma mi plan de mañana»).

| Skill | Para qué sirve | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | /battlearena:conquer | Arma o actualiza tu plan de batalla del siguiente día hábil (o el de hoy, si aún no tienes: antes de las 16:00 te pregunta cuál): revisa lo que hiciste hoy (git, tus sesiones de Claude, tus pendientes), te entrevista tema por tema (valor de negocio, DoD, ≤ 3 h, riesgos) y crea tu ticket del día (el de mañana, desde las 16:00 de la víspera) en Linear y tu orden de batalla en PDF. | | /battlearena:arena-objective | Nombra un objetivo nuevo cuando cambias de tema, para que tu actividad se agrupe por objetivo; continuar el vigente lo hace el hook en cada turno, que también lo liga con tu plan del día. Si el colector duda, Claude te pregunta una vez «¿es parte del punto N o es emergente?» (objective.mjs link). No hace falta llamarla. | | /battlearena:arena-self-review | Tu revisión semanal de desempeño, self-service: lee tu actividad de la semana desde el MCP de Battle Arena y te da una autocrítica con evidencia. | | /battlearena:execution-coach | Interceptor mayéutico durante la ejecución: te frena con preguntas cuando el pedido es vago o la sesión da vueltas sin converger. Se activa sola cuando el hook detecta esas señales. | | /battlearena:scope-technical-task | Coach de alcance antes de codear: problema de negocio, sub-problemas, soluciones sin código, estimación en horas. | | /battlearena:pr-review-loop | Atiende la review de Copilot de un PR en segundo plano (corrige → gates → commit → resolver hilos → re-review). Siempre opt-in: pregunta antes y cuántas iteraciones tolera. |

Comandos

| Comando | Para qué sirve | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | /ringside | Abre Ringside: la pelea de tu día junto al chat. Tu plan con el avance y el tiempo de cada punto contra lo comprometido (semáforo verde · amarillo · rojo), los emergentes (clic para ver su motivo), los riesgos siempre visibles con ✋ Levantar la mano y 👍/👎, y el pie con Plan · Reuniones · Fuera · Total. Pestaña Semana con tus hábitos. Sin datos, te dice el siguiente paso (instalar, armar el plan, entrar a la daily). /scorecard, su nombre anterior, lo sigue abriendo con un aviso. |

Arriba del prompt sale además una banda compacta de Ringside (LER-640 ✓ 1 ↻ 1 ↷ 1 ⚠ 1 Abrir, cada contador con su color).

Modo cierre (ADR-0036 §17). Desde las 16:00 (CDMX) el día del consultor ya cerró: hasta las 9:00 del siguiente día hábil (el fin de semana, con el cierre del viernes) el panel es el resultado del día en vez de un día nuevo en 0m. Lleva el marcador navy (minutos en tu plan contra fuera de plan y la barra plan / fuera / reuniones), cómo terminó cada punto (avanzado, «pasó de Xh», «sin tocar», entregado ✓; 📅 si tuvo reunión ligada), cuántos emergentes justificaste con «Abrir chat ↗» para los que faltan y el siguiente paso, /battlearena:conquer para el plan del siguiente día. Lo trabajado después del corte no resta: sale aparte («ya llevas 25m para el viernes»). En cuanto guardas el plan del siguiente día, el panel vuelve a la vista normal de ese día. Al corte sale un aviso una vez por día («Tu jueves cerró · 6h32 en plan · 2 emergentes sin responder») y la banda dice el cierre con ✎ N por responder. En modo cierre se relee cada 5 min. La pestaña Semana muestra el último cierre con su resultado.

La radio (Arena FM) en Ringside — opcional

La rocola de la Oficina dentro de Ringside. Se instala aparte, porque es lo único que trae una dependencia (@livekit/rtc-node) y necesita ffplay (de ffmpeg: brew install ffmpeg):

npx @thebattlearena/corner@latest install --with radio

Reusa la url y la llave de ~/.claude/battle-arena/config.json (no hay que pasarlas otra vez) y deja anotado "with": ["radio"] ahí mismo: una reinstalación o actualización sin el flag no quita la radio. El reproductor (radio/player.mjs + su package.json) va a ~/.claude/battle-arena/hooks/radio/ y npm install baja su dependencia SOLO en esa carpeta; los hooks .mjs siguen sin dependencias.

  • Fila bajo las pestañas (Hoy y Semana): ♪ título · artista truncado y ⏸/▶. Sale con una canción puesta, también en pausa (… · en pausa, apagada): la rocola se pausa sola cuando nadie la escucha, y con ▶ entras como oyente y vuelve a sonar. Sin canción o sin el bot en la sala no aparece.
  • Pestaña «Radio» («Radio ♪» mientras suena): ecualizador coral/azul que late cada ~500 ms solo con esa pestaña a la vista (congelado en pausa), ⏸/▶ con la canción y lo que lleva sonando, la cola con sus votos («tuya» si la pediste) y quién escucha. Sin la radio instalada, la pestaña da el comando para activarla.
  • ▶/⏸ es local: arranca o detiene un reproductor en tu máquina (un proceso aparte, lanzado con nohup, su PID en el store del plugin); no manda comandos a la rocola ni la pausa para nadie. El reproductor pide un token de solo escucha, entra a labs-musica como oyente (audience.listening = '1'), se suscribe solo a la pista de jukebox-bot-audio y manda el audio a ffplay. Al cerrar la sesión que lo lanzó se apaga; antes de matar un PID se confirma con ps que sigue siendo el reproductor.
  • Estado y cola se leen cada 5 s solo con el panel a la vista; con el panel cerrado no se pide nada. La llave de los hooks abre jukebox/state, queue y token de /v1/labs/live-audio (votar y pedir canciones siguen en la web). La API no da la duración ni el artista: el panel muestra lo que lleva sonando y el título que arma la API.

El tema de Ghostty — opcional

Los colores de la marca en la terminal: fondo navy #061224, cursor coral #F26464, azul #2684FF y la paleta ANSI de los tokens dark de apps/web. El tema vive en themes/ghostty/Battle Arena:

npx @thebattlearena/corner@latest install --with theme

Lo copia a ~/.config/ghostty/themes/ y lo elige en la config de Ghostty que ya exista (config.ghostty o config, en ~/.config/ghostty/ o en ~/Library/Application Support/com.mitchellh.ghostty/): Ghostty las carga en orden y gana el último theme =, así que sólo toca la config que decide: la última con tema (cambia su último theme = y anota el anterior, tal cual, en un comentario) o, si ninguna tiene, la última que exista. Si ya tenías un tema «Battle Arena» propio, no lo pisa. Nunca crea la config: sin ella imprime theme = Battle Arena para que la pegues. También imprime, sin escribirlas, las líneas del look completo (JetBrains Mono, márgenes de la ventana e ícono coral en macOS). Como la radio, queda en "with" de config.json. uninstall regresa el tema que tenías y quita el archivo sólo si es el nuestro y ninguna config lo sigue eligiendo. Se ve al recargar la config de Ghostty (⌘⇧,).

Los comandos de Ringside no llevan el prefijo del plugin (así registra Claude Code los comandos de un módulo), por eso su nombre ya es único.

Hooks

Scripts zero-dependency que corren en la máquina de cada dev. NO les agregues imports de node_modules: la promesa es instalación-cero. Viven junto al plugin y su hooks/hooks.json los llama con node "${CLAUDE_PLUGIN_ROOT}/../<script>".

| Script | Hook | Qué hace | | --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- | | emit.mjs | todos | Enriquece con identidad git, sanitiza y postea a /ingest; buffer local si el colector no responde | | session-start.mjs | SessionStart | Handshake + inyecta ranking (/v1/leaderboard/rank) y alerta de rama protegida | | check-connection.mjs | UserPromptSubmit | Avisa si hay desconexión o eventos en cola | | objective-nudge.mjs | UserPromptSubmit | Registra el vigente y lo contrasta con el plan del día: refs del turno vs brief.refs (ADR-0036 §18) | | execution-coach-nudge.mjs | UserPromptSubmit | Detecta señales de ejecución subóptima y, con throttle por sesión, sugiere battlearena:execution-coach | | pr-review-worktree.mjs | PostToolUse | Al detectar gh pr create, ofrece (opt-in) battlearena:pr-review-loop; no lanza nada por sí solo |

Helpers que usan las skills: objective.mjs (el objetivo del turno) y plan.mjs (actividad local de git y el PDF de la orden de batalla de /battlearena:conquer).

Estructura

apps/claude-hooks/
  cli.mjs  install.sh  *.mjs  lib/       el paquete npm y los scripts de los hooks
  .claude-plugin/marketplace.json        marketplace local: battlearena → ./plugin
  plugin/
    .claude-plugin/plugin.json           { name: "battlearena" }
    hooks/hooks.json                     los hooks de comando + el módulo de Ringside
    hooks/register.tsx …                 Ringside (panel, banda, comandos)
    skills/                              las skills (las genera `npm run bundle:skill`)
    tests/                               pruebas de Ringside (`claude plugin test plugin`)
  radio/player.mjs  radio/package.json   reproductor de la radio (solo con `install --with radio`)
  themes/ghostty/Battle Arena            tema de Ghostty (solo con `install --with theme`)

Plan de batalla (skill /battlearena:conquer + plan.mjs, ADR-0042)

/conquer (o «arma mi plan de mañana») arma tu plan del siguiente día hábil como estratega: revisa lo que hiciste hoy, te propone los pendientes con su explicación y su ticket, y por cada tema pide contexto en Linear, valor, DoD no ambiguo, duración (≤ 3 h o «divide y vencerás» en subtareas) y riesgos. El de mañana se completa desde las 16:00 (CDMX) de la víspera (fin de semana libre); antes solo guarda el borrador.

¿Hoy o mañana? De lunes a viernes antes de las 16:00, si aún no tienes plan de hoy, te pregunta cuál armar (hoy, recomendado); desde las 16:00 o con el de hoy ya hecho, arma el de mañana. También puedes pedirlo directo («arma mi plan de hoy»). El plan de hoy se completa al momento y entra a tu reloj del día y a los recordatorios.

Cómo habla:

  • Conector MCP de Battle Arena (claude.ai → Settings → Connectors, o /mcp): plan_pending, plan_my_day, plan_get, plan_save_draft, plan_can_complete, plan_complete, plan_reopen, board_add, plan_summary. Identidad = tu cuenta OAuth (sin api-key).
  • Conector de Linear, como tú: busca tus tickets y, al completar, crea «tu ticket del día» (conquest:daily) con el borrador que da plan_can_complete. Lo que no tiene ticket NO se crea: va al tablero de la PM (board_add).
  • plan.mjs, solo local (sin api-key ni red):
node plan.mjs local-activity [--repo <ruta>]…   # git de hoy SOLO en esas rutas (sin --repo, el cwd):
                                               # rama, commits con tu email (máx. 10), archivos sin
                                               # commit (solo nombres, máx. 20) y con `gh` el PR de
                                               # la rama actual + los tuyos de los últimos 5 hábiles
node plan.mjs pdf --file summary.json [--reviewer <PM>] [--out <carpeta>] [--no-open]

pdf arma la «orden de batalla» (diseño A4 · Primero lo importante) con lo de plan_summary: HTML con el logo embebido y, si la máquina tiene Chrome/Chromium/Edge (BATTLE_ORDER_CHROME para otra ruta), el PDF con --headless --print-to-pdf en ~/Documents/Battle Arena/. Sin navegador deja el HTML listo para «Imprimir → Guardar como PDF». Es solo para ti: no se sube.

Su fuente única vive en este paquete: skill/conquer/SKILL.md (en el repo, .claude/skills/conquer es un symlink hacia ella y bundle:skill no la toca).

PR review loop (pr-review-worktree + skill battlearena:pr-review-loop)

Al crear un PR con gh pr create, el hook pr-review-worktree sugiere (no lanza) activar la skill pr-review-loop. Es opt-in: la skill primero pregunta al usuario si quiere activarlo, explicando qué hará y el costo (cada re-review consume Copilot/Actions de la org) y cuántas iteraciones tolera. Si acepta, el ciclo corre en un subagente en segundo plano con worktree aislado (no bloquea la sesión ni toca el main tree): esperar review → corregir → tsc/lint → commit + git push origin HEAD:<branch> → responder/resolver hilos → re-review, hasta converger o agotar el tope. Al terminar avisa; si no convergió, indica explícitamente qué haría continuar y vuelve a preguntar. Nunca mergea (decisión humana). También se invoca a mano: /pr-review-loop <url-o-número>.

El hook pr-review-worktree queda activo solo tras (re)instalar los hooks —npx @thebattlearena/corner@latest install o, desde el clone, bash apps/claude-hooks/install.sh (ambos pasan por cli.mjs, la misma fuente)—. Una instalación previa no lo tiene registrado hasta actualizar.

Estado local: ~/.claude/hook-stream/{status.json, buffer.ndjson} y, para el throttle del interceptor, ~/.claude/battle-arena/execution-coach/<session>.json.

El plugin hermano ring

ring es una forma de trabajar para repos de backend (Node, Python, Go…) al refactorizar o revisar un PR grande: una sesión árbitro en effort high revisa; un solo wrestler en effort medium escribe en su propio worktree, un concepto a la vez; los cornerman (sólo lectura, effort high) revisan cada rango de commits contra un reglamento numerado. El humano ve PRs chicos y temporales (round-bells, en draft y con la etiqueta do-not-merge) en vez de un PR gigante al final, y una bitácora durable (judges card) permite que otra sesión retome sin el transcript. No depende del colector ni de la api-key.

Se instala junto con battlearena, con el mismo comando y del mismo marketplace local: ring@battlearena-local. uninstall quita los dos.

| Comando | Qué hace | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | /ring:weigh-in | Escribe .claude/ring.json del repo: detecta los gates y pregunta sólo lo que no pudo | | /ring:referee start <pr> | Abre el combate: worktree del wrestler, estado, revisión y round-bells | | /ring:referee resume [<pr>] | Retoma desde el estado durable en una sesión nueva | | /ring:referee close | Gates finales, validateCommand opcional sobre la punta exacta, cuerpo del PR | | /ring:no-surrender | Modo noche: fija el /goal y deja lo que es del humano anotado, sin bloquearse | | /ring:title-defense <pr> [--max-rounds N] | Defiende el PR ante la review de Copilot: cataloga cada hallazgo, lo corrige por el mismo ciclo y re-pide review hasta quedar limpio | | /ring:play-by-play <pr> | Narra el combate como crónica deportiva con la hora real de cada golpe y la publica como página privada, siempre en el mismo link |

Agentes: ring:wrestler (ejecutor, effort: medium) y ring:cornerman (revisor de sólo lectura, effort: high, en modo rango, concepto o lote).

Config del repo (.claude/ring.json, opcional): gates (typecheck, tests por ruta con {paths}, lint), validateCommand, rulebook (por defecto el genérico del plugin), protectedBranches, forbidAiTrailers (por defecto true), heavyCommands, delegation, precedents y monitors (ver «Monitores del repo»). Sin el archivo, ring usa los gates que detecta en package.json, pyproject.toml, go.mod y el Makefile.

Delegación y precedentes: con delegation: "referee" (el default) el árbitro decide él mismo las dudas de diseño con el reglamento y los precedentes, anota por qué en decisions.md y sigue; sólo te llega una lista corta (merges y bases, flags y variables de entorno, cambios visibles para el cliente que nadie pidió, un PR que pase el techo de tamaño del repo, datos de clientes, dinero o producción, lo que bloquea mask-guard y contradicciones entre tus instrucciones). Con "human" te consulta todo, como antes. Los precedentes genéricos vienen con el plugin (PR1…PR26) y los del repo van en .claude/ring-precedents.md (HP1…); al cerrar, el árbitro te propone un PR aparte con los nuevos.

La crónica (/ring:play-by-play) sale de los registros reales: la bitácora de decisiones y rebotes, los commits, las revisiones de Copilot, los eventos del PR y las corridas de CI. Nunca inventa una hora: lo que no tiene registro se dice como hueco. Es privada; tú la compartes desde su menú. Más adelante ring podrá mandar el link de la crónica al dashboard de Battle Arena; hoy no se conecta al colector.

El contrato es del juez: si un concepto agrega o cambia un endpoint del spec de la API (apiSpec) o un modelo de base de datos (dbModels: entidades, schema, migraciones), el trabajo se detiene hasta que tú apruebas el contrato, con cualquier nivel de delegación. El wrestler escribe sólo el contrato, el árbitro lo revisa y te sirve una vista previa en http://127.0.0.1:4815 (Swagger del spec propuesto, qué cambia contra la base y el diff de modelos con las tablas y columnas), y al aprobarlo registra el hash con judges-card.mjs approve-contract. mask-guard bloquea el commit de un archivo de contrato cuyo contenido no aprobaste.

Estado de cada combate: ~/.claude/ring/<owner>/<repo>/<pr>/ (state.json mínimo, decisions.md, rebounds.md, judges-card.md). Un directorio por nivel, para que a-b/c y a/b-c no choquen; owner y repo van en minúsculas (GitHub no distingue mayúsculas) y un repo sin remote origin vive en local/<nombre>. RING_HOME cambia la raíz; config.mjs slug imprime <owner>/<repo>. Un combate del formato plano anterior (<owner>-<repo>/<pr>/) se mueve solo la primera vez que un script lo busca (las rutas absolutas de state.json y del judges card lo siguen); si existen los dos, gana el nuevo, el viejo no se toca y se avisa por stderr. Si la mudanza falla, mask-guard bloquea diciendo por qué en vez de dejar pasar. Nunca en el scratchpad de la sesión, que se borra al reiniciar.

El ring en vivo en Ringside. Si el repo de la sesión tiene un combate activo, el punto del plan cuyas ligas (refs) nombran el PR del combate (agents-studio-be#1060, owner/repo#N o su URL) dice junto a su tiempo si ahí se está trabajando: spinner mientras alguien trabaja, ■ ámbar si te toca decidir, 👊N en rojo si Copilot dejó hilos, nada en reposo. Al abrir el punto aparece la fila «▸ Revisión #N» con el tiempo del ring de hoy (si ningún punto liga el PR, la fila va sola tras los puntos). Desplegada (▾), como la lámina «Cuerpo»: la cabecera (spinner o ■ ámbar, #PR, los rounds de la defensa del título con su barra ▮▯ sacados de los ## Round N de title-defense.md contra su tope, la última round-bell y la crónica); la tarjeta en vivo con acento a la izquierda (azul trabajando, roja «👊×N HIGH» si Copilot golpeó, ámbar llena «TE TOCA DECIDIR» con el tiempo, o «sin actividad desde hace X»); las fichas en una fila, cajas redondeadas con el ícono al centro y el estado en el borde (quieta gris, activa azul con barra, en la esquina morada con ◣, golpeó roja con 👊N, te toca ámbar con «tú», abierta con borde grueso y ▾); y la ficha abierta: título con desde cuándo, el verbo con su barra, la tabla de fields del evento (rama, commits, archivo; rango, modo, veredicto; hilos, severidad; pregunta con «responde en la terminal ↵»), ÚLTIMOS y cómo cerrarla. Las fichas son cuadrados rellenos de 4×2 celdas (con celdas de ~1:2.1 se ven cuadrados), sin borde, en un tinte del color del actor (más fuerte trabajando y abierta), con el glifo centrado arriba y el indicador centrado abajo, dentro de la ficha; toda la ficha es el clic. Si la terminal dibuja imágenes (el panel lo pregunta con $.ui.blit sobre la campana de la cabecera), va el PNG del ícono en su lugar; Ghostty hoy no las dibuja con este engine (lo confirma /ringside-icons, que muestra cada forma de imagen con lo que contestó el engine), así que lo normal son los glifos, o dos letras ASCII sin la fuente. Los íconos chicos (campana, crónica, tarjeta y título) van con la fuente y, con ella instalada, todos los símbolos de Ringside (estados del punto ↻ ↷ ○ ● ✓, ▸ ▾ ▴, ↗, ✋ 👍 👎, ✎, 📅 💡, ♪ ▶ ⏸ ▲…) son sus glifos, en sus mismos colores; sin ella, los caracteres de siempre (ic() en icons.ts). La última round-bell sale del feed en vivo o, en combates de antes, de gh pr list --head ring/<pr>/round-<n> (una vez por round). Un escritor puede mandar la tabla de la ficha con fields (live.mjs emit --field rama=feat/x).

La fuente de íconos. fonts/battle-arena-icons/BattleArenaIcons.ttf: 58 glifos en el área de uso privado suplementaria A (U+FBA00–U+FBA85, lejos de los de Nerd Fonts), con nombre de glifo igual al id de src/codepoints.json. UPM 1000; un ícono de una celda avanza 600 y uno de dos, 1200 (la celda de una mono típica es 0.6 em), centrados en vertical en y = 340. 31 vienen de Lucide (ISC, LICENSE-lucide.txt) y 5 son propios. install la copia a ~/Library/Fonts (macOS) o a ~/.local/share/fonts (Linux, con fc-cache -f si está) y deja iconFont: true en ~/.claude/battle-arena/config.json: un mod no puede saber qué fuentes tiene la terminal, así que Ringside sólo dibuja glifos con esa bandera. Si Ghostty ya tiene config (config.ghostty o config, en ~/.config/ghostty/ o en ~/Library/Application Support/com.mitchellh.ghostty/), le agrega font-codepoint-map = U+FBA00-U+FBAFF=Battle Arena Icons (el bloque entero de la fuente); si no, imprime la línea para que la agregues tú (nunca crea ni reescribe tu config). uninstall quita la fuente, esa línea y apaga iconFont. Para regenerarla tras cambiar un SVG: python3 -m venv /tmp/ba-icons && /tmp/ba-icons/bin/pip install -r fonts/battle-arena-icons/requirements.txt && /tmp/ba-icons/bin/python fonts/battle-arena-icons/build.py (picosvg pasa los trazos a contornos y fonttools arma el .ttf; nada de esto corre al instalar).

La fuente es <combate>/live.jsonl, un evento JSON por línea (se queda con los últimos 60 más el último de cada actor y cada trabajo abierto). La escriben, de mejor esfuerzo y sin hacer ruido nunca: round-bell.mjs open (campana), title-defense.mjs requested|wait|feedback (Copilot en la esquina, revisó, 👊N), judges-card.mjs log|approve-contract|play-by-play-url (árbitro y juez), one-at-a-time.mjs run y el hook live-hook.mjs (PreToolUse/PostToolUse/PostToolUseFailure sobre Agent, AskUserQuestion y Bash, y SubagentStop). Convención de subagentes: cuentan los de tipo ring:wrestler / ring:cornerman y, si son general-purpose, los que empiezan su description con Wrestler: o Cornerman: (también Luchador: / Esquinero:); el resto de la descripción es la etiqueta del chip. Un AskUserQuestion pendiente pone al juez en «te toca decidir»; si la pregunta se descarta sin contestar (Claude Code no avisa), el siguiente UserPromptSubmit o Stop de esa sesión la cierra como «sin respuesta», y Ringside la da por caducada a los 30 min. Sólo las sesiones del combate escriben: una sesión se registra sola (state.json → sessions, con su rol y cuándo se vio) la primera vez que corre un script del ring; sus subagentes comparten su session_id. Otra sesión que trabaje en el mismo repo no cuenta.

Monitores del repo. ring es genérico y no trae monitores: cada repo declara los suyos, con sus íconos y colores, en SU .claude/ring.json, y salen como fichas extra tras las del ring. Un ejemplo, el de agents-studio-be (su batería de campañas, que además abre la vista del mod campaign-test-monitor; no es parte del plugin):

{
  "monitors": [
    {
      "id": "bateria",
      "label": "Batería",
      "icon": "bateria",
      "color": "green",
      "match": { "command": "campaign-mock-report/scripts/run\\.mjs" },
      "open": { "command": "campaign-test-monitor" }
    }
  ]
}
  • id (minúsculas, dígitos, _, -; nunca el de un actor del ring) y label. Una entrada inválida se ignora con un aviso en config.mjs show; nunca tumba mask-guard.

  • icon (opcional): cualquiera de los 58 nombres de la fuente Battle Arena Icons; uno que no exista, o ninguno, deja las dos primeras letras del label (los de puntos, acciones y medios son de la interfaz de Ringside y no tienen imagen, sólo glifo):

    • marca: arena
    • actores: arbitro, luchador, esquinero, campana, copilot, juez
    • estados: activo, en-la-esquina, golpe, listo, fallo, espera, te-toca, pensando, modificando, probando, revisando, subiendo, finalizando
    • objetos: pr, ronda, cronica, monitor, bateria, rama, ticket
    • ringside: plan, punto, emergente, riesgo, reunion, objetivo, tiempo, trofeo, fuego
    • puntos: en-curso, desplazado, por-hacer, con-avance, decision-pendiente
    • acciones: expandir, contraer, cerrar-detalle, abrir-web, cerrar, levantar-mano, voto-a-favor, voto-en-contra, responder, enviar, siguiente
    • medios: calendario, cabinas, radio, reproducir, pausar, votar-cancion
  • color (opcional): uno de la paleta (slate, blue, teal, amber, purple, orange, green, red, pink); sin él, verde. Va en el borde de la ficha; el ícono de un monitor se dibuja en gris neutro (sólo los actores del ring tienen PNG en su color).

  • match.command (opcional): regex sobre lo que el comando de Bash ejecuta. Antes de probarla se quitan los cuerpos de heredoc (<<'EOF' … EOF, <<EOF … EOF) y los textos entre comillas con espacios (una comilla con una sola palabra, como una ruta, se queda sin las comillas), y se prueba tramo por tramo (;, &&, ||, |): escribir un script que menciona scripts/run.mjs no activa la batería, correrlo sí. Mientras corre uno que la cumple, el monitor está activo (PreToolUse → PostToolUse/PostToolUseFailure), con la descripción del Bash como etiqueta o, sin ella, el programa que corrió (node run.mjs): nunca el comando ni una ruta entera (toda ruta absoluta se corta a su último tramo). Ese evento lleva source: "hook" y su id, y sólo cierra lo suyo: nunca termina una corrida que un script abrió con emit. Un comando en segundo plano no cuenta, para eso está emit. Un comando que corre por one-at-a-time lo marca el candado mientras lo tiene, en primer plano o en segundo, y el hook de Bash lo deja pasar.

  • open (opcional): { "command": "<comando sin />" } pone un botón «abrir ↗» en el detalle del chip que corre ese comando (por ejemplo el de otro mod que abre su propio panel); si el comando no existe en la sesión, o con { "pane": "<id>" } (un mod no puede abrir el panel de otro), el detalle dice qué escribir.

  • Procesos largos fuera de una herramienta (una batería en segundo plano): node "$RING/scripts/live.mjs" emit --monitor bateria --state probando --label "t1 de 6" --progress 1/6 [--detail "…"] [--done|--failed] [--pr <n>] || true. Sin --pr busca el combate de esa carpeta. Con RING_LIVE_FILE en el entorno (one-at-a-time se la pasa a su hijo) escribe directo ahí, y un script puede agregar él mismo la línea JSON ({"at":"<ISO>","monitor":"bateria","state":"active","label":"probando","progress":{"done":1,"total":6}}) sin depender de ring. Esquema: actor (uno del ring) o monitor (declarado), state (active, waiting, done, failed), label (≤3 palabras) y, opcionales, note, detail, progress, id (para cerrar justo ese trabajo), count, severity, pr, url, fields, health, reason y source. Un monitor no declarado se rechaza (emit sale con 1) y Ringside lo ignora. En la ficha, un valor de fields que es una ruta se ve como link a su último tramo (file://; en Terminal.app, un botón que lo abre), nunca la ruta entera, y un valor largo se recorta a un renglón con «…».

  • Salud (cualquier monitor o actor, cualquier repo): cada evento puede decir cómo fue ese paso con health (ok, warn o fail) y por qué con reason (corto, ≤80). Desde un script: emit … --health warn --reason "t1 sin medir"; --failed es siempre fail y --done es ok salvo que --health diga otra cosa. La salud de una ficha es la peor de su corrida en curso (desde el evento que sigue al último done/failed de un script; ese cierre cuenta en ella): fail gana a warn y warn a ok, así que un paso que falló o no se midió sigue avisando mientras corren los siguientes, y la corrida nueva empieza limpia. Se ve en la barrita de la ficha (verde, ámbar o rojo, con ✓ ⚠ ✗), en una línea bajo el verbo del detalle («⚠ va con avisos: t1 sin medir», «✓ va bien», «✗ falla: t3 dobles 2»), en cada renglón de ÚLTIMOS, y —sólo warn y fail— junto al tiempo del punto y en la banda «Revisión #N». Sin health en ningún paso, nada cambia. Una batería de pruebas, por ejemplo:

    emit() { node "$RING/scripts/live.mjs" emit --monitor bateria "$@" || true; }
    emit --state preparando
    emit --state probando --label "t1 de 6" --progress 1/6 --health warn --reason "t1 sin medir"
    emit --state probando --label "t2 de 6" --progress 2/6 --health ok
    emit --state probando --label "t3 de 6" --progress 3/6 --health fail --reason "t3 dobles 2"
    emit --state terminó --done          # la corrida queda en fail (la peor); la siguiente empieza limpia

mask-guard (hook PreToolUse sobre Bash) bloquea siempre git add -A/./--all, git stash pop, git reset --hard con el árbol sucio y, si forbidAiTrailers está activo, los commits con trailers de atribución de IA (los coautores humanos pasan). Sólo mientras hay un combate activo en ese repo bloquea además lo que decide el humano: merges (y git pull que no sea --ff-only/--rebase), cambios de base, cerrar o sacar de draft PRs, gh pr review, pedir review a Copilot, force push, push a una rama protegida, comandos pesados fuera de one-at-a-time y commits cuyo mensaje lleva ids internos del combate (D12, PR5, HP3; P7 o D1 sólo avisan). Y si la crónica (/ring:play-by-play) ya tiene liga y algún registro del combate (decisions.md, rebounds.md, title-defense.md, judges-card.md, round-*.md) cambió después de su última publicación, bloquea el re-pedido a Copilot y judges-card.mjs close hasta republicarla. Todo PR que abre ring nace en draft y sólo tú lo marcas listo; /ring:title-defense arranca cuando sacas el PR de draft, y un hallazgo que una reproducción dirigida (el escenario o el spec de ese bug, minutos) puede mostrar no se marca corregido ni se resuelve hasta que esa reproducción sale verde sobre la punta exacta; la validación completa (validateCommand, horas) corre una sola vez al final, sobre la punta final. Durante /ring:title-defense se permite, sólo para ese PR, re-pedir review a Copilot y responder y resolver los hilos ya corregidos; descartar reviews nunca.

Qué se censura antes de enviar (lib/sanitize.mjs)

Antes de salir de tu máquina, cada evento pasa por lib/sanitize.mjs (patrones en lib/secret-patterns.mjs):

  • Paths de tu máquina: tu homedir y /Users/<x> / /home/<x> → ~.
  • Variables sensibles por nombre (PASSWORD, *_TOKEN, *_SECRET, api_key, AUTH, y en estilo env KEY / *_KEY): se conserva el nombre y el valor queda en ***.
  • Llaves de proveedor sueltas en cualquier texto (comando, resultado, prompt): Retell, OpenAI / Anthropic, Linear, GitHub, Slack, Stripe, Google, JWT y AWS; se conserva el prefijo (sk-***).
  • Headers y URLs: x-api-key, Authorization (con o sin Bearer), ?api_key= / ?token= y credenciales en URIs (scheme://user:***@host).
  • Archivos .env leídos (Read/Grep de un .env/.env.*/*.env, o cat/head/tail/grep de uno por Bash): cada línea NOMBRE=valor del resultado queda NOMBRE=***.
  • Strings de más de 4000 caracteres se recortan.

El colector aplica el mismo filtro al ingerir (defensa en profundidad para instalaciones viejas): packages/contexts/arena/domain/services/secret-redaction.service.ts. La paridad entre ambos la cuidan vectores compartidos (packages/contexts/arena/domain/__tests__/fixtures/secret-redaction.vectors.json) que corren las dos suites: yarn workspace @thebattlearena/corner test (o node --test apps/claude-hooks/test/*.test.mjs) y el jest de la raíz.

Manejo de errores del colector

emit.mjs clasifica la respuesta de /ingest:

  • 400 / 413 / 422 (evento malformado/enorme) → se descarta (no se reintenta).
  • timeout / 5xx / 429 → se bufferea y se reenvía al primer envío exitoso.
  • 401 / 403 (API key inválida o revocada, p. ej. tras rotar la key o reiniciar el backend) → no se bufferea (reintentar con la misma key es en vano). Se marca authError en status.json y los hooks de superficie (session-start, check-connection) le piden al usuario que genere una key nueva y reinstale con npx @thebattlearena/corner@latest install --url <url> --api-key <NUEVA_KEY>. Tras un envío exitoso con la key nueva, el flag se limpia solo. Lógica compartida en lib/reauth.mjs.

Qué hace el instalador

npx @thebattlearena/corner@latest install (o migrate, que es lo mismo) requiere Claude Code CLI (claude en el PATH) y node ≥ 18:

  1. Copia los scripts y el plugin a ~/.claude/battle-arena/hooks/ (la misma ruta de antes).

  2. Guarda la url y la llave en ~/.claude/battle-arena/config.json (con installedBy, para saber si la instalación anterior era de battle-arena-hooks y avisar la migración).

  3. Limpia la versión anterior: quita sus hooks de ~/.claude/settings.json (si no, cada evento se mandaría dos veces) y mueve sus skills sueltas de ~/.claude/skills a ~/.claude/battle-arena/legacy-skills/<fecha>/ (respaldo, no las borra).

  4. Registra la carpeta como marketplace local e instala battlearena@battlearena-local para tu usuario (claude plugin marketplace add + claude plugin install --scope user). Se lee directo de esa carpeta: para actualizar basta volver a correr el instalador y /reload-plugins.

  5. Con --with radio y/o --with theme (o si ya los habías pedido), instala además la radio de Ringside y el tema de Ghostty (ver «La radio (Arena FM) en Ringside» y «El tema de Ghostty»). Correr install sin --url/--api-key reusa los de config.json.

Desde el clone (contribuidores): HOOK_STREAM_URL=… HOOK_STREAM_API_KEY=… bash apps/claude-hooks/install.sh (acepta --with radio o --with theme al final). Para probar el plugin sin instalarlo: npm run bundle:skill y claude --plugin-dir apps/claude-hooks/plugin.

Configuración

Los hooks y Ringside leen, en orden de prioridad:

  1. Variables de entorno HOOK_STREAM_URL / HOOK_STREAM_API_KEY (si están seteadas).
  2. ~/.claude/battle-arena/config.json ({ "url": "...", "apiKey": "..." }), que escribe el instalador.
  3. Default http://127.0.0.1:4500.

HOOK_STREAM_API_KEY con ARENA_REQUIRE_AUTH=true necesita scopes arena:read (handshake, ranking, Ringside), arena:write (objetivos) y arena:hands (✋ desde Ringside); /ingest acepta cualquier api-key válida.

Cutover desde el colector legacy (speech-analytics/tooling/hook-stream)

  1. Levanta battle-arena en el puerto 4500 (apagando antes el yarn hook-stream legacy) — la ruta /ingest y el compat /objective son idénticos: los hooks viejos siguen funcionando sin tocarlos.
  2. Restaura el histórico si quieres conservar el leaderboard: pg_dump de la DB hook_stream → battle_arena (mismas tablas/columnas).
  3. Actualiza tus settings para apuntar a estos hooks (ruta absoluta al clone) y borra los del repo legacy cuando todo el equipo haya migrado.