@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 migratemigrate 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 installtambié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. /scorecardsigue 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
- 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). - Escribe
/ringside(o «Abrir» en la banda) para ver el panel completo. Con × escondes la banda; vuelve al abrir el panel. - Si aún no tienes plan, Ringside te dice el siguiente paso:
/battlearena:conquerarma el de hoy (antes de las 16:00) o el del siguiente día hábil. - Trabaja normal. El objetivo de cada turno lo registra el hook; cuando cambias de tema,
/battlearena:arena-objectivenombra 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 radioBajo 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 radioReusa 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 · artistatruncado 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 alabs-musicacomo oyente (audience.listening = '1'), se suscribe solo a la pista dejukebox-bot-audioy manda el audio a ffplay. Al cerrar la sesión que lo lanzó se apaga; antes de matar un PID se confirma conpsque 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,queueytokende/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 themeLo 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 daplan_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-worktreequeda activo solo tras (re)instalar los hooks —npx @thebattlearena/corner@latest installo, desde el clone,bash apps/claude-hooks/install.sh(ambos pasan porcli.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) ylabel. Una entrada inválida se ignora con un aviso enconfig.mjs show; nunca tumbamask-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 dellabel(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
- marca:
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 mencionascripts/run.mjsno 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 llevasource: "hook"y su id, y sólo cierra lo suyo: nunca termina una corrida que un script abrió conemit. Un comando en segundo plano no cuenta, para eso estáemit. Un comando que corre porone-at-a-timelo 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--prbusca el combate de esa carpeta. ConRING_LIVE_FILEen el entorno (one-at-a-timese 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 dering. Esquema:actor(uno del ring) omonitor(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,reasonysource. Un monitor no declarado se rechaza (emitsale con 1) y Ringside lo ignora. En la ficha, un valor defieldsque 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,warnofail) y por qué conreason(corto, ≤80). Desde un script:emit … --health warn --reason "t1 sin medir";--failedes siemprefaily--doneesoksalvo que--healthdiga otra cosa. La salud de una ficha es la peor de su corrida en curso (desde el evento que sigue al últimodone/failedde un script; ese cierre cuenta en ella):failgana awarnywarnaok, 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ólowarnyfail— junto al tiempo del punto y en la banda «Revisión #N». Sinhealthen 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 envKEY/*_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 sinBearer),?api_key=/?token=y credenciales en URIs (scheme://user:***@host). - Archivos
.envleídos (Read/Grep de un.env/.env.*/*.env, ocat/head/tail/grepde uno por Bash): cada líneaNOMBRE=valordel resultado quedaNOMBRE=***. - 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
authErrorenstatus.jsony los hooks de superficie (session-start,check-connection) le piden al usuario que genere una key nueva y reinstale connpx @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 enlib/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:
Copia los scripts y el plugin a
~/.claude/battle-arena/hooks/(la misma ruta de antes).Guarda la url y la llave en
~/.claude/battle-arena/config.json(coninstalledBy, para saber si la instalación anterior era debattle-arena-hooksy avisar la migración).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/skillsa~/.claude/battle-arena/legacy-skills/<fecha>/(respaldo, no las borra).Registra la carpeta como marketplace local e instala
battlearena@battlearena-localpara 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.Con
--with radioy/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»). Correrinstallsin--url/--api-keyreusa 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:
- Variables de entorno
HOOK_STREAM_URL/HOOK_STREAM_API_KEY(si están seteadas). ~/.claude/battle-arena/config.json({ "url": "...", "apiKey": "..." }), que escribe el instalador.- 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)
- Levanta battle-arena en el puerto 4500 (apagando antes el
yarn hook-streamlegacy) — la ruta/ingesty el compat/objectiveson idénticos: los hooks viejos siguen funcionando sin tocarlos. - Restaura el histórico si quieres conservar el leaderboard:
pg_dumpde la DBhook_stream→battle_arena(mismas tablas/columnas). - Actualiza tus settings para apuntar a estos hooks (ruta absoluta al clone) y borra los del repo legacy cuando todo el equipo haya migrado.
